diff --git a/library/build.gradle b/library/build.gradle index 97790b00e7..2791591454 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -21,6 +21,7 @@ buildscript { classpath 'com.android.tools.build:gradle:9.2.1' classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.5.9' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8" classpath "org.jacoco:org.jacoco.core:$jacoco_version" classpath "org.jacoco:org.jacoco.report:$jacoco_version" @@ -35,6 +36,7 @@ plugins { apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' +apply plugin: 'org.jetbrains.kotlin.plugin.serialization' apply from: "$rootProject.projectDir/jacoco.gradle" apply plugin: "com.github.spotbugs" apply plugin: 'io.gitlab.arturbosch.detekt' diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAllSelectableLabelsRemoteOperation.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAllSelectableLabelsRemoteOperation.kt new file mode 100644 index 0000000000..5edccadebe --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAllSelectableLabelsRemoteOperation.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.EntityLabels +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.SEPARATOR +import com.owncloud.android.lib.ocs.ocsJson +import com.owncloud.android.lib.resources.OCSRemoteOperation +import org.apache.commons.httpclient.HttpStatus + +class GetAllSelectableLabelsRemoteOperation( + private val entityType: String, + private val entityId: Long +) : OCSRemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val getMethod = + GetMethod( + client.baseUri.toString() + ENDPOINT + entityType + SEPARATOR + entityId + AVAILABLE + JSON_FORMAT, + true + ) + return try { + val status = client.execute(getMethod) + if (status != HttpStatus.SC_OK) { + return RemoteOperationResult(false, getMethod) + } + val response = + ocsJson.decodeFromString>( + getMethod.getResponseBodyAsString() + ) + val data = response.ocs.data + RemoteOperationResult(true, getMethod).apply { resultData = data } + } catch (e: Exception) { + failure(e) + } finally { + getMethod.releaseConnection() + } + } + + @Suppress("DEPRECATION") + private fun failure(e: Exception): RemoteOperationResult = + RemoteOperationResult(e).also { + Log_OC.e(TAG, "Get all selectable labels failed: " + it.logMessage, it.exception) + } + + companion object { + private val TAG = GetAllSelectableLabelsRemoteOperation::class.java.simpleName + private const val ENDPOINT = "/ocs/v2.php/apps/governance/v1/labels/" + private const val AVAILABLE = "/available" + } +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableHoldLabelsRemoteOperation.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableHoldLabelsRemoteOperation.kt new file mode 100644 index 0000000000..6b4fb0a1ac --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableHoldLabelsRemoteOperation.kt @@ -0,0 +1,62 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.HoldLabelInfo +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.SEPARATOR +import com.owncloud.android.lib.ocs.ocsJson +import com.owncloud.android.lib.resources.OCSRemoteOperation +import org.apache.commons.httpclient.HttpStatus + +class GetAvailableHoldLabelsRemoteOperation( + private val entityType: String, + private val entityId: Long +) : OCSRemoteOperation>() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult> { + val getMethod = + GetMethod( + client.baseUri.toString() + ENDPOINT + entityType + SEPARATOR + entityId + + HOLD_AVAILABLE + JSON_FORMAT, + true + ) + return try { + val status = client.execute(getMethod) + if (status != HttpStatus.SC_OK) { + return RemoteOperationResult(false, getMethod) + } + val response = + ocsJson.decodeFromString>>( + getMethod.getResponseBodyAsString() + ) + val data = response.ocs.data + RemoteOperationResult>(true, getMethod).apply { resultData = data } + } catch (e: Exception) { + failure(e) + } finally { + getMethod.releaseConnection() + } + } + + @Suppress("DEPRECATION") + private fun failure(e: Exception): RemoteOperationResult> = + RemoteOperationResult>(e).also { + Log_OC.e(TAG, "Get available hold labels failed: " + it.logMessage, it.exception) + } + + companion object { + private val TAG = GetAvailableHoldLabelsRemoteOperation::class.java.simpleName + private const val ENDPOINT = "/ocs/v2.php/apps/governance/v1/labels/" + private const val HOLD_AVAILABLE = "/hold/available" + } +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableRetentionLabelsRemoteOperation.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableRetentionLabelsRemoteOperation.kt new file mode 100644 index 0000000000..0bf686482c --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableRetentionLabelsRemoteOperation.kt @@ -0,0 +1,62 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.RetentionLabelInfo +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.SEPARATOR +import com.owncloud.android.lib.ocs.ocsJson +import com.owncloud.android.lib.resources.OCSRemoteOperation +import org.apache.commons.httpclient.HttpStatus + +class GetAvailableRetentionLabelsRemoteOperation( + private val entityType: String, + private val entityId: Long +) : OCSRemoteOperation>() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult> { + val getMethod = + GetMethod( + client.baseUri.toString() + ENDPOINT + entityType + SEPARATOR + entityId + + RETENTION_AVAILABLE + JSON_FORMAT, + true + ) + return try { + val status = client.execute(getMethod) + if (status != HttpStatus.SC_OK) { + return RemoteOperationResult(false, getMethod) + } + val response = + ocsJson.decodeFromString>>( + getMethod.getResponseBodyAsString() + ) + val data = response.ocs.data + RemoteOperationResult>(true, getMethod).apply { resultData = data } + } catch (e: Exception) { + failure(e) + } finally { + getMethod.releaseConnection() + } + } + + @Suppress("DEPRECATION") + private fun failure(e: Exception): RemoteOperationResult> = + RemoteOperationResult>(e).also { + Log_OC.e(TAG, "Get available retention labels failed: " + it.logMessage, it.exception) + } + + companion object { + private val TAG = GetAvailableRetentionLabelsRemoteOperation::class.java.simpleName + private const val ENDPOINT = "/ocs/v2.php/apps/governance/v1/labels/" + private const val RETENTION_AVAILABLE = "/retention/available" + } +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableSensitivityLabelsRemoteOperation.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableSensitivityLabelsRemoteOperation.kt new file mode 100644 index 0000000000..df4e355a9a --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetAvailableSensitivityLabelsRemoteOperation.kt @@ -0,0 +1,62 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.SensitivityLabelInfo +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.SEPARATOR +import com.owncloud.android.lib.ocs.ocsJson +import com.owncloud.android.lib.resources.OCSRemoteOperation +import org.apache.commons.httpclient.HttpStatus + +class GetAvailableSensitivityLabelsRemoteOperation( + private val entityType: String, + private val entityId: Long +) : OCSRemoteOperation>() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult> { + val getMethod = + GetMethod( + client.baseUri.toString() + ENDPOINT + entityType + SEPARATOR + entityId + + SENSITIVITY_AVAILABLE + JSON_FORMAT, + true + ) + return try { + val status = client.execute(getMethod) + if (status != HttpStatus.SC_OK) { + return RemoteOperationResult(false, getMethod) + } + val response = + ocsJson.decodeFromString>>( + getMethod.getResponseBodyAsString() + ) + val data = response.ocs.data + RemoteOperationResult>(true, getMethod).apply { resultData = data } + } catch (e: Exception) { + failure(e) + } finally { + getMethod.releaseConnection() + } + } + + @Suppress("DEPRECATION") + private fun failure(e: Exception): RemoteOperationResult> = + RemoteOperationResult>(e).also { + Log_OC.e(TAG, "Get available sensitivity labels failed: " + it.logMessage, it.exception) + } + + companion object { + private val TAG = GetAvailableSensitivityLabelsRemoteOperation::class.java.simpleName + private const val ENDPOINT = "/ocs/v2.php/apps/governance/v1/labels/" + private const val SENSITIVITY_AVAILABLE = "/sensitivity/available" + } +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetEntityLabelsRemoteOperation.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetEntityLabelsRemoteOperation.kt new file mode 100644 index 0000000000..5de6d55d6f --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/GetEntityLabelsRemoteOperation.kt @@ -0,0 +1,60 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.EntityLabels +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.SEPARATOR +import com.owncloud.android.lib.ocs.ocsJson +import com.owncloud.android.lib.resources.OCSRemoteOperation +import org.apache.commons.httpclient.HttpStatus + +class GetEntityLabelsRemoteOperation( + private val entityType: String, + private val entityId: String +) : OCSRemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val getMethod = + GetMethod( + client.baseUri.toString() + ENDPOINT + entityType + SEPARATOR + entityId + JSON_FORMAT, + true + ) + return try { + val status = client.execute(getMethod) + if (status != HttpStatus.SC_OK) { + return RemoteOperationResult(false, getMethod) + } + val response = + ocsJson.decodeFromString>( + getMethod.getResponseBodyAsString() + ) + val data = response.ocs.data + RemoteOperationResult(true, getMethod).apply { resultData = data } + } catch (e: Exception) { + failure(e) + } finally { + getMethod.releaseConnection() + } + } + + @Suppress("DEPRECATION") + private fun failure(e: Exception): RemoteOperationResult = + RemoteOperationResult(e).also { + Log_OC.e(TAG, "Get entity labels failed: " + it.logMessage, it.exception) + } + + companion object { + private val TAG = GetEntityLabelsRemoteOperation::class.java.simpleName + private const val ENDPOINT = "/ocs/v2.php/apps/governance/v1/labels/" + } +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/RemoveLabelRemoteOperation.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/RemoveLabelRemoteOperation.kt new file mode 100644 index 0000000000..293208f04c --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/RemoveLabelRemoteOperation.kt @@ -0,0 +1,64 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.GovernanceLabelResponse +import com.nextcloud.android.lib.resources.governance.model.LabelType +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.DeleteMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.ocsJson +import com.owncloud.android.lib.resources.OCSRemoteOperation +import org.apache.commons.httpclient.HttpStatus + +class RemoveLabelRemoteOperation( + private val entityType: String, + private val entityId: Long, + private val labelType: LabelType, + private val labelId: String +) : OCSRemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val deleteMethod = + DeleteMethod( + client.baseUri.toString() + ENDPOINT + entityType + SEPARATOR + entityId + SEPARATOR + + labelType.value + SEPARATOR + labelId + JSON_FORMAT, + true + ) + return try { + val status = client.execute(deleteMethod) + if (status != HttpStatus.SC_OK) { + return RemoteOperationResult(false, deleteMethod) + } + val response = + ocsJson.decodeFromString>( + deleteMethod.getResponseBodyAsString() + ) + val data = response.ocs.data + RemoteOperationResult(true, deleteMethod).apply { resultData = data } + } catch (e: Exception) { + failure(e) + } finally { + deleteMethod.releaseConnection() + } + } + + @Suppress("DEPRECATION") + private fun failure(e: Exception): RemoteOperationResult = + RemoteOperationResult(e).also { + Log_OC.e(TAG, "Remove label from entity failed: " + it.logMessage, it.exception) + } + + companion object { + private val TAG = RemoveLabelRemoteOperation::class.java.simpleName + private const val ENDPOINT = "/ocs/v2.php/apps/governance/v1/labels/" + private const val SEPARATOR = "/" + } +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/SetLabelRemoteOperation.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/SetLabelRemoteOperation.kt new file mode 100644 index 0000000000..e2a5b494e1 --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/SetLabelRemoteOperation.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.GovernanceLabelResponse +import com.nextcloud.android.lib.resources.governance.model.LabelType +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.PostMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.SEPARATOR +import com.owncloud.android.lib.ocs.ocsJson +import com.owncloud.android.lib.resources.OCSRemoteOperation +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import org.apache.commons.httpclient.HttpStatus + +class SetLabelRemoteOperation( + private val entityType: String, + private val entityId: Long, + private val labelType: LabelType, + private val labelId: String +) : OCSRemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val body = "".toRequestBody("application/json".toMediaTypeOrNull()) + val postMethod = + PostMethod( + client.baseUri.toString() + ENDPOINT + entityType + SEPARATOR + entityId + SEPARATOR + + labelType.value + SEPARATOR + labelId + JSON_FORMAT, + true, + body + ) + return try { + val status = client.execute(postMethod) + if (status != HttpStatus.SC_OK) { + return RemoteOperationResult(false, postMethod) + } + val response = + ocsJson.decodeFromString>( + postMethod.getResponseBodyAsString() + ) + val data = response.ocs.data + RemoteOperationResult(true, postMethod).apply { resultData = data } + } catch (e: Exception) { + failure(e) + } finally { + postMethod.releaseConnection() + } + } + + @Suppress("DEPRECATION") + private fun failure(e: Exception): RemoteOperationResult = + RemoteOperationResult(e).also { + Log_OC.e(TAG, "Apply label to entity failed: " + it.logMessage, it.exception) + } + + companion object { + private val TAG = SetLabelRemoteOperation::class.java.simpleName + private const val ENDPOINT = "/ocs/v2.php/apps/governance/v1/labels/" + } +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/EntityLabels.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/EntityLabels.kt new file mode 100644 index 0000000000..7cb246e114 --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/EntityLabels.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance.model + +import kotlinx.serialization.Serializable + +@Serializable +data class EntityLabels( + val sensitivity: List, + val retention: List, + val hold: List +) diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/GovernanceLabelResponse.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/GovernanceLabelResponse.kt new file mode 100644 index 0000000000..ea06cb8d41 --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/GovernanceLabelResponse.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance.model + +import kotlinx.serialization.Serializable + +@Serializable +data class GovernanceLabelResponse( + val message: String +) diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/HoldLabelInfo.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/HoldLabelInfo.kt new file mode 100644 index 0000000000..16a6c2a123 --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/HoldLabelInfo.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance.model + +import kotlinx.serialization.Serializable + +@Serializable +data class HoldLabelInfo( + val id: String, + val name: String, + val priority: Long, + val description: String, + val color: String, + val scopes: List = emptyList() +) diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/LabelScope.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/LabelScope.kt new file mode 100644 index 0000000000..f631090abd --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/LabelScope.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class LabelScope { + @SerialName("FILES") + FILES, + + @SerialName("MAILS") + MAILS +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/LabelType.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/LabelType.kt new file mode 100644 index 0000000000..712d5f1fba --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/LabelType.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance.model + +enum class LabelType( + val value: String +) { + SENSITIVITY("SENSITIVITY"), + RETENTION("RETENTION"), + HOLD("HOLD") +} diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/RetentionLabelInfo.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/RetentionLabelInfo.kt new file mode 100644 index 0000000000..a40035dbca --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/RetentionLabelInfo.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance.model + +import kotlinx.serialization.Serializable + +@Serializable +data class RetentionLabelInfo( + val id: String, + val name: String, + val priority: Long, + val description: String, + val color: String, + val scopes: List = emptyList() +) diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/SensitivityLabelInfo.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/SensitivityLabelInfo.kt new file mode 100644 index 0000000000..5427a9b323 --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/SensitivityLabelInfo.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance.model + +import kotlinx.serialization.Serializable + +@Serializable +data class SensitivityLabelInfo( + val id: String, + val name: String, + val priority: Long, + val description: String, + val color: String, + val scopes: List = emptyList() +) diff --git a/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/SensitivityLabelScope.kt b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/SensitivityLabelScope.kt new file mode 100644 index 0000000000..d0e5f79935 --- /dev/null +++ b/library/src/main/java/com/nextcloud/android/lib/resources/governance/model/SensitivityLabelScope.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class SensitivityLabelScope { + @SerialName("FILES") + FILES, + + @SerialName("MAILS") + MAILS +} 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 new file mode 100644 index 0000000000..5bd6a6806f --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/ocs/OcsKotlinResponse.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.ocs + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +const val SEPARATOR = "/" + +/** + * Strict [Json] used to decode OCS responses with kotlinx.serialization. + * + * Unknown keys (e.g. the OCS `meta` wrapper or additive server fields) are ignored, but required + * fields are enforced: a missing or type-mismatched field fails the decode immediately (fail fast) + * instead of silently producing a half-populated object. + */ +internal val ocsJson = Json { ignoreUnknownKeys = true } + +/** + * Serializable counterpart of [ServerResponse]: the outer `{ "ocs": { "data": ... } }` envelope + * shared by every OCS endpoint. Reusable for any endpoint whose payload type [T] is `@Serializable`. + */ +@Serializable +internal data class OcsKotlinResponse( + val ocs: OcsKotlin +) + +@Serializable +internal data class OcsKotlin( + val data: T +) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/status/GetCapabilitiesRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/status/GetCapabilitiesRemoteOperation.java index 3aca2ca3f7..d1a96fce37 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/status/GetCapabilitiesRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/status/GetCapabilitiesRemoteOperation.java @@ -162,6 +162,9 @@ public class GetCapabilitiesRemoteOperation extends RemoteOperation>(json).ocs.data + + assertEquals("Label applied", data.message) + } +} diff --git a/library/src/test/java/com/nextcloud/android/lib/resources/governance/HoldLabelInfoParsingTest.kt b/library/src/test/java/com/nextcloud/android/lib/resources/governance/HoldLabelInfoParsingTest.kt new file mode 100644 index 0000000000..f696c54fd5 --- /dev/null +++ b/library/src/test/java/com/nextcloud/android/lib/resources/governance/HoldLabelInfoParsingTest.kt @@ -0,0 +1,65 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.HoldLabelInfo +import com.nextcloud.android.lib.resources.governance.model.LabelScope +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.ocsJson +import org.junit.Assert.assertEquals +import org.junit.Test + +class HoldLabelInfoParsingTest { + @Test + fun parsesAvailableHoldLabelsResponse() { + val json = + """ + { + "ocs": { + "meta": { "status": "ok", "statuscode": 200, "message": "OK" }, + "data": [ + { + "id": "litigation-hold", + "name": "Litigation hold", + "priority": 30, + "description": "Preserve for legal proceedings", + "color": "#ff0000", + "scopes": ["FILES", "MAILS"] + }, + { + "id": "no-hold", + "name": "No hold", + "priority": 10, + "description": "No legal hold applied", + "color": "#00ff00", + "scopes": ["FILES"] + } + ] + } + } + """.trimIndent() + + val labels = + ocsJson + .decodeFromString>>(json) + .ocs + .data + + assertEquals(2, labels.size) + + val first = labels[0] + assertEquals("litigation-hold", first.id) + assertEquals("Litigation hold", first.name) + assertEquals(30L, first.priority) + assertEquals("Preserve for legal proceedings", first.description) + assertEquals("#ff0000", first.color) + assertEquals(listOf(LabelScope.FILES, LabelScope.MAILS), first.scopes) + + assertEquals(listOf(LabelScope.FILES), labels[1].scopes) + } +} diff --git a/library/src/test/java/com/nextcloud/android/lib/resources/governance/LabelTypeTest.kt b/library/src/test/java/com/nextcloud/android/lib/resources/governance/LabelTypeTest.kt new file mode 100644 index 0000000000..c651a791cd --- /dev/null +++ b/library/src/test/java/com/nextcloud/android/lib/resources/governance/LabelTypeTest.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.LabelType +import org.junit.Assert.assertEquals +import org.junit.Test + +class LabelTypeTest { + @Test + fun pathValuesMatchSpecEnum() { + assertEquals("SENSITIVITY", LabelType.SENSITIVITY.value) + assertEquals("RETENTION", LabelType.RETENTION.value) + assertEquals("HOLD", LabelType.HOLD.value) + } +} diff --git a/library/src/test/java/com/nextcloud/android/lib/resources/governance/RetentionLabelInfoParsingTest.kt b/library/src/test/java/com/nextcloud/android/lib/resources/governance/RetentionLabelInfoParsingTest.kt new file mode 100644 index 0000000000..eff665e6d3 --- /dev/null +++ b/library/src/test/java/com/nextcloud/android/lib/resources/governance/RetentionLabelInfoParsingTest.kt @@ -0,0 +1,65 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.LabelScope +import com.nextcloud.android.lib.resources.governance.model.RetentionLabelInfo +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.ocsJson +import org.junit.Assert.assertEquals +import org.junit.Test + +class RetentionLabelInfoParsingTest { + @Test + fun parsesAvailableRetentionLabelsResponse() { + val json = + """ + { + "ocs": { + "meta": { "status": "ok", "statuscode": 200, "message": "OK" }, + "data": [ + { + "id": "keep-10-years", + "name": "Keep 10 years", + "priority": 30, + "description": "Retain for ten years", + "color": "#ff0000", + "scopes": ["FILES", "MAILS"] + }, + { + "id": "no-retention", + "name": "No retention", + "priority": 10, + "description": "May be deleted any time", + "color": "#00ff00", + "scopes": ["FILES"] + } + ] + } + } + """.trimIndent() + + val labels = + ocsJson + .decodeFromString>>(json) + .ocs + .data + + assertEquals(2, labels.size) + + val first = labels[0] + assertEquals("keep-10-years", first.id) + assertEquals("Keep 10 years", first.name) + assertEquals(30L, first.priority) + assertEquals("Retain for ten years", first.description) + assertEquals("#ff0000", first.color) + assertEquals(listOf(LabelScope.FILES, LabelScope.MAILS), first.scopes) + + assertEquals(listOf(LabelScope.FILES), labels[1].scopes) + } +} diff --git a/library/src/test/java/com/nextcloud/android/lib/resources/governance/SensitivityLabelInfoParsingTest.kt b/library/src/test/java/com/nextcloud/android/lib/resources/governance/SensitivityLabelInfoParsingTest.kt new file mode 100644 index 0000000000..89bc3f7e96 --- /dev/null +++ b/library/src/test/java/com/nextcloud/android/lib/resources/governance/SensitivityLabelInfoParsingTest.kt @@ -0,0 +1,65 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.SensitivityLabelInfo +import com.nextcloud.android.lib.resources.governance.model.SensitivityLabelScope +import com.owncloud.android.lib.ocs.OcsKotlinResponse +import com.owncloud.android.lib.ocs.ocsJson +import org.junit.Assert.assertEquals +import org.junit.Test + +class SensitivityLabelInfoParsingTest { + @Test + fun parsesAvailableSensitivityLabelsResponse() { + val json = + """ + { + "ocs": { + "meta": { "status": "ok", "statuscode": 200, "message": "OK" }, + "data": [ + { + "id": "confidential", + "name": "Confidential", + "priority": 30, + "description": "Restricted to a small group", + "color": "#ff0000", + "scopes": ["FILES", "MAILS"] + }, + { + "id": "public", + "name": "Public", + "priority": 10, + "description": "Anyone may access", + "color": "#00ff00", + "scopes": ["FILES"] + } + ] + } + } + """.trimIndent() + + val labels = + ocsJson + .decodeFromString>>(json) + .ocs + .data + + assertEquals(2, labels.size) + + val first = labels[0] + assertEquals("confidential", first.id) + assertEquals("Confidential", first.name) + assertEquals(30L, first.priority) + assertEquals("Restricted to a small group", first.description) + assertEquals("#ff0000", first.color) + assertEquals(listOf(SensitivityLabelScope.FILES, SensitivityLabelScope.MAILS), first.scopes) + + assertEquals(listOf(SensitivityLabelScope.FILES), labels[1].scopes) + } +}