diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index 38a3214fc3..f8162ee550 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -1946,6 +1946,34 @@ ZTZhNjlAZ21haWwuY29tPg== =JYcM -----END PGP PUBLIC KEY BLOCK----- +pub E0CB7823CFD00FBF +uid Jake Wharton + +sub F3DBCE882C3A01AA +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsBNBFsNoY0BCADIvRrJEX3k7UeuT6zt+F4++xH+5Qo7QzdicjFhhyb22PLPyIsI +Ema+T4QqiPDegUv8yKKTTBmHNw/vSUHTPX9ZUpglckopuOgdfnuQjTKEOEzrN7V/ +ZGLB8yR+BoKEJBLdp6W4fGHIxza/o8Iwg8nV0/C9X1gvQI0I63aexqqA5Y0eYt4y +12mQur5sKabJRRWxkn1U8oQmcFs+P/KPVtS4lUb1nsGzFJbsiKY8AOI1RbnYgtIu +c6mzpiUk0NPO27xKScyDq7lKBwwOZMwRzX/6yX7jcAtyTWZnWS675MCbT2K07ubU +Ykrs9NObwDXlKZWLk6JuADgzQaueYIrjLRYxABEBAAG0H0pha2UgV2hhcnRvbiA8 +amFrZXdAZ29vZ2xlLmNvbT7OwE0EWw2hjQEIAMvs0IpYeLX3fWxo7uRF2s4T3juq +cyocto97QnyZUsixgTQ+e0ETlwFHPdUBSLtwkbqhdYpoN8RyUo9vkGjLZJkD0znS +BS6xea/WNJj18uBP2QgPXNtM1Upi8pEReZ5RBq0boYz+jbd3cbG5TDAEYQolSmCg +Z2TuugIucMYP9NumoKDdidEwunGjyi7Spk7FsgdrF9BapdaiYrSQqAsXwpH+m1b/ +m41ItWF42fW/mpOk8XaEdzGA8xLSATpQ89AWnYi93fSr7ETodUffn51n36fz16M5 +W9CM1tHMdHa5gmdLvRh+Bx1MU985wxTiDUfwDN+6k1fbX+t84ULH6asLNtEAEQEA +AcLAfAQYAQgAJhYhBEe/WSJhzRqKabcDtODLeCPP0A+/BQJbDaGNAhsMBQkDwmcA +AAoJEODLeCPP0A+/SUwH/0Osdy2hl3iII7OJyO//y3eL1kzmj7PyWj4sMJdK375U +bs5xsUct/e49qU/Q/oz0zig+lHaYSnfLFMx4qpftW6VMZ/iGl/mekXoCdOrUxZ/0 ++srpfJMpIDJyozdZ7n3bFtlq2no1Tbm/kyUrwu3M8Q/a2Lkh8jwygLBGqluz0PfN +sAECAGlxpqGwS1vWjrhHtPIfRb3UD3Ho++OFmz5zKKObjs0x7AlcYIr7Oe+Da2Mv +TMgZUgKMGqsJhvdHsMqZRKV6aVbSdPs3PJgt8UtD2waA4lLRA5xlLdgTdN5dp+Tm +u4awI7kDfdqz1V5s5prLGlkoPhACUxRZeznT3Js7NVE= +=hrDF +-----END PGP PUBLIC KEY BLOCK----- + pub E16AB52D79FD224F sub 5A34A5E06B936F93 -----BEGIN PGP PUBLIC KEY BLOCK----- diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2fdb638a9f..f550bb86a7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -125,6 +125,7 @@ + @@ -8237,14 +8238,14 @@ - - - - - - - - + + + + + + + + @@ -8365,14 +8366,14 @@ - - - - - - - - + + + + + + + + @@ -8493,14 +8494,14 @@ - - - - - - - - + + + + + + + + @@ -8735,6 +8736,14 @@ + + + + + + + + diff --git a/library/build.gradle b/library/build.gradle index 32b7c61773..9e5b61ce13 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -18,7 +18,7 @@ buildscript { junit_version = '4.13.2' } dependencies { - classpath 'com.android.tools.build:gradle:9.4.0' + classpath 'com.android.tools.build:gradle:9.4.1' classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.5.11' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" @@ -64,7 +64,9 @@ dependencies { implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' api 'com.squareup.okhttp3:okhttp:5.5.0' - implementation 'com.github.bitfireAT:dav4jvm:3.0.2' + api 'com.github.bitfireAT:dav4jvm:2.2.1' + api 'com.squareup.retrofit2:retrofit:3.0.0' + api 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0' // in transition phase, we use old and new libs implementation group: 'com.google.code.gson', name: 'gson', version: '2.14.0' implementation 'androidx.annotation:annotation:1.10.0' diff --git a/library/src/androidTest/java/com/nextcloud/android/lib/resources/webdav/DavResourcePropfindIT.kt b/library/src/androidTest/java/com/nextcloud/android/lib/resources/webdav/DavResourcePropfindIT.kt new file mode 100644 index 0000000000..f97bb85a69 --- /dev/null +++ b/library/src/androidTest/java/com/nextcloud/android/lib/resources/webdav/DavResourcePropfindIT.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.lib.resources.webdav + +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.property.DisplayName +import at.bitfire.dav4jvm.property.ResourceType +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.ocs.SEPARATOR +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Showcases dav4jvm (OkHttp-based) as an alternative to the [com.nextcloud.operations] OkHttp + * methods: a PROPFIND with `Depth: 1` on a user's WebDAV root, reusing the same authenticated + * OkHttpClient as [com.owncloud.android.AbstractIT.nextcloudClient]. + */ +class DavResourcePropfindIT : AbstractIT() { + @Test + fun propfindRootFolderReturnsCollection() { + val davResource = DavResource(buildDavHttpClient(), rootFolderUrl()) + + var selfResponse: Response? = null + davResource.propfind(PROPFIND_DEPTH, DisplayName.NAME, ResourceType.NAME) { response, relation -> + if (relation == Response.HrefRelation.SELF) { + selfResponse = response + } + } + + val resourceType = selfResponse?.get(ResourceType::class.java) + assertTrue("PROPFIND response must include the requested collection itself", selfResponse != null) + assertTrue( + "root resource must be reported as a collection", + resourceType?.types?.contains(ResourceType.COLLECTION) == true + ) + } + + private fun buildDavHttpClient(): OkHttpClient = + nextcloudClient.client + .newBuilder() + .followRedirects(false) + .addNetworkInterceptor { chain -> + val request = + chain + .request() + .newBuilder() + .header("Authorization", nextcloudClient.credentials) + .header("User-Agent", OwnCloudClientManagerFactory.getUserAgent()) + .build() + chain.proceed(request) + }.build() + + private fun rootFolderUrl(): HttpUrl = "${nextcloudClient.filesDavUri}$SEPARATOR".toHttpUrl() + + companion object { + private const val PROPFIND_DEPTH = 1 + } +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/users/UserApi.kt b/library/src/main/java/com/nextcloud/android/lib/resources/users/UserApi.kt new file mode 100644 index 0000000000..43de1ca884 --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/users/UserApi.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.lib.resources.users + +import com.nextcloud.android.lib.resources.users.model.UserInfoResponse +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.Query + +private const val ENDPOINT_USER = "ocs/v2.php/cloud/user" +private const val QUERY_FORMAT = "format" +private const val OCS_API_REQUEST_HEADER = "OCS-APIREQUEST: true" + +/** + * Retrofit definition of the OCS user endpoint. The caller supplies a [retrofit2.Retrofit] + * whose OkHttpClient adds authentication, e.g. one derived from [com.nextcloud.common.NextcloudClient.client]. + */ +interface UserApi { + @Headers(OCS_API_REQUEST_HEADER) + @GET(ENDPOINT_USER) + fun getUser( + @Query(QUERY_FORMAT) format: String + ): Call> + + @Headers(OCS_API_REQUEST_HEADER) + @GET(ENDPOINT_USER) + suspend fun fetchUser( + @Query(QUERY_FORMAT) format: String + ): OcsKotlinResponse +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/users/model/QuotaResponse.kt b/library/src/main/java/com/nextcloud/android/lib/resources/users/model/QuotaResponse.kt new file mode 100644 index 0000000000..c13901b684 --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/users/model/QuotaResponse.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.lib.resources.users.model + +import kotlinx.serialization.Serializable + +@Serializable +data class QuotaResponse( + val free: Long = 0, + val used: Long = 0, + val total: Long = 0, + val relative: Double = 0.0, + val quota: Long = 0 +) diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/users/model/UserInfoResponse.kt b/library/src/main/java/com/nextcloud/android/lib/resources/users/model/UserInfoResponse.kt new file mode 100644 index 0000000000..9ce5ea1fbb --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/users/model/UserInfoResponse.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.lib.resources.users.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfoResponse( + val id: String? = null, + val enabled: Boolean? = null, + @SerialName("display-name") + val displayName: String? = null, + val email: String? = null, + val phone: String? = null, + val address: String? = null, + val website: String? = null, + val twitter: String? = null, + val quota: QuotaResponse? = null, + val groups: List? = null +) diff --git a/library/src/main/java/com/owncloud/android/lib/ocs/OcsKotlinResponse.kt b/library/src/main/java/com/owncloud/android/lib/ocs/OcsKotlinResponse.kt index 5bd6a6806f..b4b4e2774e 100644 --- a/library/src/main/java/com/owncloud/android/lib/ocs/OcsKotlinResponse.kt +++ b/library/src/main/java/com/owncloud/android/lib/ocs/OcsKotlinResponse.kt @@ -26,11 +26,11 @@ internal val ocsJson = Json { ignoreUnknownKeys = true } * shared by every OCS endpoint. Reusable for any endpoint whose payload type [T] is `@Serializable`. */ @Serializable -internal data class OcsKotlinResponse( +data class OcsKotlinResponse( val ocs: OcsKotlin ) @Serializable -internal data class OcsKotlin( +data class OcsKotlin( val data: T ) diff --git a/library/src/test/java/com/nextcloud/android/lib/resources/users/UserInfoResponseParsingTest.kt b/library/src/test/java/com/nextcloud/android/lib/resources/users/UserInfoResponseParsingTest.kt new file mode 100644 index 0000000000..821d4af0a4 --- /dev/null +++ b/library/src/test/java/com/nextcloud/android/lib/resources/users/UserInfoResponseParsingTest.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.lib.resources.users + +import com.nextcloud.android.lib.resources.users.model.UserInfoResponse +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.ocsJson +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class UserInfoResponseParsingTest { + @Test + fun parsesUserInfoResponse() { + val json = + """ + { + "ocs": { + "meta": { "status": "ok", "statuscode": 200, "message": "OK" }, + "data": { + "id": "user1", + "enabled": true, + "display-name": "User One", + "email": "user1@example.com", + "quota": { + "free": 100, + "used": 50, + "total": 150, + "relative": 33.3, + "quota": -3 + }, + "groups": ["admin", "users"] + } + } + } + """.trimIndent() + + val userInfo = + ocsJson + .decodeFromString>(json) + .ocs + .data + + assertEquals("user1", userInfo.id) + assertEquals("User One", userInfo.displayName) + assertEquals("user1@example.com", userInfo.email) + assertEquals(-3L, userInfo.quota?.quota) + assertEquals(listOf("admin", "users"), userInfo.groups) + assertNull(userInfo.phone) + } +} diff --git a/sample_client/build.gradle b/sample_client/build.gradle index a8299c0432..1045edda44 100644 --- a/sample_client/build.gradle +++ b/sample_client/build.gradle @@ -11,7 +11,7 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:9.4.0' + classpath 'com.android.tools.build:gradle:9.4.1' classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.5.11' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8"