From 8a5a8f83b643e4d0cfa879f8a68f22ce568779fd Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Wed, 9 Sep 2026 18:14:33 +0200 Subject: [PATCH 1/6] feat: show trashbin icon when user has permission Signed-off-by: Jorge Aguado Recio --- .../sharing/GraphShareFragment.kt | 27 ++++++++++++++++++- .../sharing/GraphShareViewModel.kt | 23 ++++++++++++++++ .../sharing/GraphSharesAdapter.kt | 12 +++++++-- .../sharing/GraphSharesDiffUtil.kt | 3 ++- owncloudApp/src/main/res/values/strings.xml | 1 + 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt index 62e9f23f9a9..5595b72659e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt @@ -55,6 +55,7 @@ class GraphShareFragment : Fragment() { private var roles: List = emptyList() private var listener: GraphShareFragmentListener? = null + private var canRemoveShares: Boolean = false override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = MembersFragmentBinding.inflate(inflater, container, false) @@ -107,6 +108,7 @@ class GraphShareFragment : Fragment() { private fun subscribeToViewModels() { observeRoles() observeShares() + observeSpacePermissions() observeAddShareResult() } @@ -139,7 +141,7 @@ class GraphShareFragment : Fragment() { val hasMembers = it.members.isNotEmpty() binding.membersRecyclerView.isVisible = hasMembers binding.noSharesMessage.isVisible = !hasMembers - graphSharesAdapter.setShares(it.members, it.roles) + graphSharesAdapter.setShares(it.members, it.roles, canRemoveShares) binding.swipeRefreshMembers.isRefreshing = false } } @@ -154,6 +156,28 @@ class GraphShareFragment : Fragment() { } } + private fun observeSpacePermissions() { + collectLatestLifecycleFlow(graphShareViewModel.spacePermissions) { event -> + event?.let { + when (val uiResult = event.peekContent()) { + is UIResult.Success -> { + uiResult.data?.let { spacePermissions -> + checkPermissions(spacePermissions) + } + } + is UIResult.Loading -> { } + is UIResult.Error -> { + Timber.e(uiResult.error, "Failed to retrieve space permissions") + } + } + } + } + } + + private fun checkPermissions(spacePermissions: List) { + canRemoveShares = DRIVES_DELETE_PERMISSION in spacePermissions + } + private fun observeAddShareResult() { collectLatestLifecycleFlow(graphShareViewModel.addShareResultFlow) { event -> event?.peekContent()?.let { uiResult -> @@ -176,6 +200,7 @@ class GraphShareFragment : Fragment() { companion object { private const val ARG_FILE = "FILE" private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME" + private const val DRIVES_DELETE_PERMISSION = "libre.graph/driveItem/permissions/delete" fun newInstance(file: OCFile, accountName: String): GraphShareFragment { val args = Bundle().apply { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt index 4b683896144..5e29b0f9516 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt @@ -35,6 +35,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.AddGraphShareAsyncUse import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUseCase import com.owncloud.android.domain.sharing.shares.model.OCPermissions import com.owncloud.android.domain.user.usecases.GetUserIdAsyncUseCase +import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase import com.owncloud.android.domain.utils.Event import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.presentation.common.UIResult @@ -55,6 +56,7 @@ class GraphShareViewModel( private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase, private val searchMembersUseCase: SearchMembersUseCase, private val getUserIdAsyncUseCase: GetUserIdAsyncUseCase, + private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase, private val accountName: String, private val file: OCFile, private val coroutineDispatcherProvider: CoroutinesDispatcherProvider, @@ -81,6 +83,9 @@ class GraphShareViewModel( private var searchJob: Job? = null var capabilities: OCCapability? = null + private val _spacePermissions = MutableStateFlow>>?>(null) + val spacePermissions: StateFlow>>?> = _spacePermissions + init { runUseCaseWithResult( coroutineDispatcher = coroutineDispatcherProvider.io, @@ -98,6 +103,24 @@ class GraphShareViewModel( viewModelScope.launch(coroutineDispatcherProvider.io) { capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName)) } + getSpacePermissions() + } + + fun getSpacePermissions() { + val spaceId = file.spaceId + if (spaceId == null) { + _spacePermissions.update { Event(UIResult.Error(error = IncompleteFileDataException())) } + return + } + + runUseCaseWithResult( + coroutineDispatcher = coroutineDispatcherProvider.io, + flow = _spacePermissions, + useCase = getSpacePermissionsAsyncUseCase, + useCaseParams = GetSpacePermissionsAsyncUseCase.Params(accountName = accountName, spaceId = spaceId), + showLoading = false, + requiresConnection = true + ) } fun getGraphShares() { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt index bc765fe678c..0991e03da19 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt @@ -37,6 +37,7 @@ class GraphSharesAdapter : RecyclerView.Adapter = emptyList() private var rolesMap: Map = emptyMap() + private var canRemoveShares = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphShareViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -58,6 +59,11 @@ class GraphSharesAdapter : RecyclerView.Adapter, roles: List) { + fun setShares(shares: List, roles: List, canRemoveShares: Boolean) { + val hasUserPermissionsChanged = this.canRemoveShares != canRemoveShares + this.canRemoveShares = canRemoveShares this.rolesMap = roles.associate { it.id to it.displayName } val sortedShares = shares.sortedWith( compareBy { it.isGroup } .thenBy { it.displayName.lowercase() } ) - val diffResult = DiffUtil.calculateDiff(GraphSharesDiffUtil(this.shares, sortedShares)) + val diffResult = DiffUtil.calculateDiff(GraphSharesDiffUtil(this.shares, sortedShares, hasUserPermissionsChanged)) this.shares = sortedShares diffResult.dispatchUpdatesTo(this) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesDiffUtil.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesDiffUtil.kt index 39bf62427ee..20f330211a7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesDiffUtil.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesDiffUtil.kt @@ -26,6 +26,7 @@ import com.owncloud.android.domain.sharing.shares.model.MemberPermission class GraphSharesDiffUtil( private val oldList: List, private val newList: List, + private val hasUserPermissionsChanged: Boolean = false, ) : DiffUtil.Callback() { override fun getOldListSize(): Int = oldList.size @@ -36,5 +37,5 @@ class GraphSharesDiffUtil( oldList[oldItemPosition].id == newList[newItemPosition].id override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] + oldList[oldItemPosition] == newList[newItemPosition] && !hasUserPermissionsChanged } diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 1e3d2b988c6..aac07aedfb0 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -729,6 +729,7 @@ Remove password Generate password Copy password + Remove share %1$s Create a shortcut URL From 2c347c99b5327b42fe572490cec6a633c3b34e15 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Fri, 11 Sep 2026 14:57:55 +0200 Subject: [PATCH 2/6] feat: show confirmation dialog when removing a share Signed-off-by: Jorge Aguado Recio --- .../presentation/sharing/GraphShareFragment.kt | 16 ++++++++++++++-- .../presentation/sharing/GraphSharesAdapter.kt | 11 ++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt index 5595b72659e..0c560af4600 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt @@ -20,6 +20,7 @@ package com.owncloud.android.presentation.sharing +import android.app.AlertDialog import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -32,6 +33,8 @@ import com.owncloud.android.R import com.owncloud.android.databinding.MembersFragmentBinding import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.roles.model.OCRole +import com.owncloud.android.domain.sharing.shares.model.MemberPermission +import com.owncloud.android.extensions.avoidScreenshotsIfNeeded import com.owncloud.android.extensions.collectLatestLifecycleFlow import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar @@ -40,7 +43,7 @@ import org.koin.androidx.viewmodel.ext.android.activityViewModel import org.koin.core.parameter.parametersOf import timber.log.Timber -class GraphShareFragment : Fragment() { +class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterListener { private var _binding: MembersFragmentBinding? = null private val binding get() = _binding!! @@ -66,7 +69,7 @@ class GraphShareFragment : Fragment() { super.onViewCreated(view, savedInstanceState) binding.membersTitle.text = getString(R.string.share_with_people_title) - graphSharesAdapter = GraphSharesAdapter() + graphSharesAdapter = GraphSharesAdapter(this) binding.membersRecyclerView.apply { layoutManager = LinearLayoutManager(requireContext()) adapter = graphSharesAdapter @@ -105,6 +108,15 @@ class GraphShareFragment : Fragment() { _binding = null } + override fun onRemoveShare(share: MemberPermission) { + AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.confirmation_remove_share_message, share.displayName)) + .setPositiveButton(getString(R.string.common_yes)) { _, _ -> } + .setNegativeButton(getString(R.string.common_no)) { dialog, _ -> dialog.dismiss() } + .show() + .avoidScreenshotsIfNeeded() + } + private fun subscribeToViewModels() { observeRoles() observeShares() diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt index 0991e03da19..018e9516ec5 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt @@ -33,7 +33,9 @@ import com.owncloud.android.domain.sharing.shares.model.MemberPermission import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.PreferenceUtils -class GraphSharesAdapter : RecyclerView.Adapter() { +class GraphSharesAdapter( + private val listener: GraphSharesAdapterListener, +) : RecyclerView.Adapter() { private var shares: List = emptyList() private var rolesMap: Map = emptyMap() @@ -62,6 +64,9 @@ class GraphSharesAdapter : RecyclerView.Adapter Date: Tue, 15 Sep 2026 11:46:21 +0200 Subject: [PATCH 3/6] feat: implement methods and network operation to remove a share on an oCIS server Signed-off-by: Jorge Aguado Recio --- .../dependecyinjection/UseCaseModule.kt | 2 + .../sharing/GraphShareFragment.kt | 19 ++++- .../sharing/GraphShareViewModel.kt | 28 +++++++ owncloudApp/src/main/res/values/strings.xml | 2 + .../shares/RemoveRemoteGraphShareOperation.kt | 76 +++++++++++++++++++ .../resources/shares/services/ShareService.kt | 6 ++ .../services/implementation/OCShareService.kt | 8 ++ .../datasources/RemoteShareDataSource.kt | 7 ++ .../implementation/OCRemoteShareDataSource.kt | 11 +++ .../shares/repository/OCShareRepository.kt | 7 ++ .../domain/sharing/shares/ShareRepository.kt | 7 ++ .../usecases/RemoveGraphShareAsyncUseCase.kt | 40 ++++++++++ 12 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/RemoveRemoteGraphShareOperation.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/RemoveGraphShareAsyncUseCase.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 94e509739fc..fb6bb8f7cfd 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -100,6 +100,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUs import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase +import com.owncloud.android.domain.sharing.shares.usecases.RemoveGraphShareAsyncUseCase import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase import com.owncloud.android.domain.spaces.usecases.DisableSpaceUseCase import com.owncloud.android.domain.spaces.usecases.EditSpaceImageUseCase @@ -241,6 +242,7 @@ val useCaseModule = module { factoryOf(::GetShareesAsyncUseCase) factoryOf(::GetSharesAsLiveDataUseCase) factoryOf(::RefreshSharesFromServerAsyncUseCase) + factoryOf(::RemoveGraphShareAsyncUseCase) // Spaces factoryOf(::CreateSpaceUseCase) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt index 0c560af4600..ed0b5564359 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt @@ -111,7 +111,7 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList override fun onRemoveShare(share: MemberPermission) { AlertDialog.Builder(requireContext()) .setMessage(getString(R.string.confirmation_remove_share_message, share.displayName)) - .setPositiveButton(getString(R.string.common_yes)) { _, _ -> } + .setPositiveButton(getString(R.string.common_yes)) { _, _ -> graphShareViewModel.removeGraphShare(share.id) } .setNegativeButton(getString(R.string.common_no)) { dialog, _ -> dialog.dismiss() } .show() .avoidScreenshotsIfNeeded() @@ -122,6 +122,7 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList observeShares() observeSpacePermissions() observeAddShareResult() + observeRemoveShareResult() } private fun observeRoles() { @@ -205,6 +206,22 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList } } + private fun observeRemoveShareResult() { + collectLatestLifecycleFlow(graphShareViewModel.removeShareResultFlow) { uiResult -> + when (uiResult) { + is UIResult.Loading -> { } + is UIResult.Success -> { + showMessageInSnackbar(getString(R.string.share_remove_correctly)) + graphShareViewModel.getGraphShares() + } + is UIResult.Error -> { + showErrorInSnackbar(R.string.share_remove_failed, uiResult.error) + Timber.e(uiResult.error, "Failed to remove a graph share") + } + } + } + } + interface GraphShareFragmentListener { fun addGraphShare(file: OCFile, accountName: String) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt index 5e29b0f9516..40b6c958d54 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt @@ -33,6 +33,7 @@ import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase import com.owncloud.android.domain.sharing.shares.usecases.AddGraphShareAsyncUseCase import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUseCase +import com.owncloud.android.domain.sharing.shares.usecases.RemoveGraphShareAsyncUseCase import com.owncloud.android.domain.sharing.shares.model.OCPermissions import com.owncloud.android.domain.user.usecases.GetUserIdAsyncUseCase import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase @@ -57,6 +58,7 @@ class GraphShareViewModel( private val searchMembersUseCase: SearchMembersUseCase, private val getUserIdAsyncUseCase: GetUserIdAsyncUseCase, private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase, + private val removeGraphShareAsyncUseCase: RemoveGraphShareAsyncUseCase, private val accountName: String, private val file: OCFile, private val coroutineDispatcherProvider: CoroutinesDispatcherProvider, @@ -80,6 +82,9 @@ class GraphShareViewModel( private val _addShareResultFlow = MutableStateFlow>?>(null) val addShareResultFlow: StateFlow>?> = _addShareResultFlow + private val _removeShareResultFlow = MutableSharedFlow>() + val removeShareResultFlow: SharedFlow> = _removeShareResultFlow + private var searchJob: Job? = null var capabilities: OCCapability? = null @@ -167,6 +172,29 @@ class GraphShareViewModel( ) } + fun removeGraphShare(shareId: String) { + val spaceId = file.spaceId + val itemId = file.remoteId + if (spaceId == null || itemId == null) { + viewModelScope.launch(coroutineDispatcherProvider.io) { + _removeShareResultFlow.emit(UIResult.Error(error = IncompleteFileDataException())) + } + return + } + + runUseCaseWithResult( + coroutineDispatcher = coroutineDispatcherProvider.io, + sharedFlow = _removeShareResultFlow, + useCase = removeGraphShareAsyncUseCase, + useCaseParams = RemoveGraphShareAsyncUseCase.Params( + accountName = accountName, + spaceId = spaceId, + itemId = itemId, + shareId = shareId, + ) + ) + } + fun searchMembers(query: String) { searchJob?.cancel() searchJob = viewModelScope.launch(coroutineDispatcherProvider.io) { diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index aac07aedfb0..aa5220eb3b6 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -497,6 +497,8 @@ Share created correctly Share could not be created Is already shared with this user/group + Share removed correctly + Share could not be removed Public links Create link share Edit link share diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/RemoveRemoteGraphShareOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/RemoveRemoteGraphShareOperation.kt new file mode 100644 index 00000000000..e6094e74e78 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/RemoveRemoteGraphShareOperation.kt @@ -0,0 +1,76 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.lib.resources.shares + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.methods.nonwebdav.DeleteMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import timber.log.Timber +import java.net.URL + +class RemoveRemoteGraphShareOperation( + private val spaceId: String, + private val itemId: String, + private val shareId: String +) : RemoteOperation() { + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult + try { + val uriBuilder = client.baseUri.buildUpon().apply { + appendEncodedPath(GRAPH_API_DRIVES_PATH) + appendEncodedPath(spaceId) + appendEncodedPath(GRAPH_API_ITEMS_PATH) + appendEncodedPath(itemId) + appendEncodedPath(GRAPH_API_PERMISSIONS_PATH) + appendEncodedPath(shareId) + } + + val deleteMethod = DeleteMethod(URL(uriBuilder.build().toString())) + + val status = client.executeHttpMethod(deleteMethod) + + val response = deleteMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_NO_CONTENT) { + Timber.d("Successful response: $response") + result = RemoteOperationResult(ResultCode.OK) + Timber.d("Remove graph share operation completed") + } else { + result = RemoteOperationResult(deleteMethod) + Timber.e("Failed response while removing a graph share; status code: $status, response: $response") + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Timber.e(e, "Exception while removing a graph share") + } + return result + } + + companion object { + private const val GRAPH_API_DRIVES_PATH = "graph/v1beta1/drives/" + private const val GRAPH_API_ITEMS_PATH = "items" + private const val GRAPH_API_PERMISSIONS_PATH = "permissions" + } +} + diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/ShareService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/ShareService.kt index e9a4f0c4455..0c1b64911ac 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/ShareService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/ShareService.kt @@ -50,6 +50,12 @@ interface ShareService : Service { expirationDate: String? ): RemoteOperationResult + fun removeGraphShare( + spaceId: String, + itemId: String, + shareId: String + ): RemoteOperationResult + fun insertShare( remoteFilePath: String, shareType: ShareType, diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/implementation/OCShareService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/implementation/OCShareService.kt index f22bdc472e3..06bbc78863a 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/implementation/OCShareService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/implementation/OCShareService.kt @@ -32,6 +32,7 @@ import com.owncloud.android.lib.resources.shares.AddRemoteGraphShareOperation import com.owncloud.android.lib.resources.shares.CreateRemoteShareOperation import com.owncloud.android.lib.resources.shares.GetRemoteGraphSharesForFileOperation import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation +import com.owncloud.android.lib.resources.shares.RemoveRemoteGraphShareOperation import com.owncloud.android.lib.resources.shares.RemoveRemoteShareOperation import com.owncloud.android.lib.resources.shares.ShareResponse import com.owncloud.android.lib.resources.shares.ShareType @@ -63,6 +64,13 @@ class OCShareService(override val client: OwnCloudClient) : ShareService { ): RemoteOperationResult = AddRemoteGraphShareOperation(spaceId, itemId, memberId, memberType, roleId, expirationDate).execute(client) + override fun removeGraphShare( + spaceId: String, + itemId: String, + shareId: String + ): RemoteOperationResult = + RemoveRemoteGraphShareOperation(spaceId, itemId, shareId).execute(client) + override fun insertShare( remoteFilePath: String, shareType: ShareType, diff --git a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/RemoteShareDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/RemoteShareDataSource.kt index eb9d4178aa4..0c217862d2c 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/RemoteShareDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/RemoteShareDataSource.kt @@ -50,6 +50,13 @@ interface RemoteShareDataSource { expirationDate: String? ) + fun removeGraphShare( + accountName: String, + spaceId: String, + itemId: String, + shareId: String + ) + fun insert( remoteFilePath: String, shareType: ShareType, diff --git a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSource.kt index ddb0fa5fe78..3e0915292c9 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSource.kt @@ -83,6 +83,17 @@ class OCRemoteShareDataSource( } } + override fun removeGraphShare( + accountName: String, + spaceId: String, + itemId: String, + shareId: String + ) { + executeRemoteOperation { + clientManager.getShareService(accountName).removeGraphShare(spaceId, itemId, shareId) + } + } + override fun insert( remoteFilePath: String, shareType: ShareType, diff --git a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepository.kt index f09f19a33c8..603a92ba36f 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepository.kt @@ -133,6 +133,13 @@ class OCShareRepository( expirationDate: String? ) = remoteShareDataSource.addGraphShare(accountName, spaceId, itemId, member, roleId, expirationDate) + override fun removeGraphShare( + accountName: String, + spaceId: String, + itemId: String, + shareId: String + ) = remoteShareDataSource.removeGraphShare(accountName, spaceId, itemId, shareId) + override fun refreshSharesFromNetwork( filePath: String, accountName: String diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/ShareRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/ShareRepository.kt index 6f70df52236..4842e603e62 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/ShareRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/ShareRepository.kt @@ -93,6 +93,13 @@ interface ShareRepository { expirationDate: String? ) + fun removeGraphShare( + accountName: String, + spaceId: String, + itemId: String, + shareId: String + ) + fun refreshSharesFromNetwork(filePath: String, accountName: String) fun deleteShare(remoteId: String, accountName: String) diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/RemoveGraphShareAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/RemoveGraphShareAsyncUseCase.kt new file mode 100644 index 00000000000..775345a4e82 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/RemoveGraphShareAsyncUseCase.kt @@ -0,0 +1,40 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.sharing.shares.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.sharing.shares.ShareRepository + +class RemoveGraphShareAsyncUseCase( + private val shareRepository: ShareRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Params) = + shareRepository.removeGraphShare(params.accountName, params.spaceId, params.itemId, params.shareId) + + data class Params( + val accountName: String, + val spaceId: String, + val itemId: String, + val shareId: String + ) +} + From 782a987816c455fc3628a0982987cd67e28490fd Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Tue, 15 Sep 2026 12:00:08 +0200 Subject: [PATCH 4/6] test: create new tests for OCShareRepositoryTest and OCRemoteShareDataSourceTest Signed-off-by: Jorge Aguado Recio --- .../OCRemoteShareDataSourceTest.kt | 30 +++++++++++++++++++ .../repository/OCShareRepositoryTest.kt | 29 ++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/owncloudData/src/test/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSourceTest.kt index ac5f7b28dec..7849c89784e 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSourceTest.kt @@ -56,6 +56,7 @@ class OCRemoteShareDataSourceTest { private val clientManager: ClientManager = mockk(relaxed = true) private val userType = OCMemberType.toString(OC_USER_MEMBER.type).lowercase() + private val shareId = "share-id" @Before fun setUp() { @@ -450,6 +451,35 @@ class OCRemoteShareDataSourceTest { } } + @Test + fun `removeGraphShare removes a graph share correctly`() { + val removeGraphShareResult = createRemoteOperationResultMock(Unit, isSuccess = true) + + every { + ocShareService.removeGraphShare( + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId + ) + } returns removeGraphShareResult + + ocRemoteShareDataSource.removeGraphShare( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId + ) + + verify(exactly = 1) { + clientManager.getShareService(OC_ACCOUNT_NAME) + ocShareService.removeGraphShare( + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId + ) + } + } + @Test(expected = ShareNotFoundException::class) fun `insert throws a ShareNotFoundException when share is not found`() { insertShareOperationWithError(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND) diff --git a/owncloudData/src/test/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepositoryTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepositoryTest.kt index 12063015db5..a3ef043c4a5 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepositoryTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepositoryTest.kt @@ -61,6 +61,7 @@ class OCShareRepositoryTest { private val password = "password" private val permissions = OC_SHARE.permissions private val expiration = RemoteShare.INIT_EXPIRATION_DATE_IN_MILLIS + private val shareId = "share-id" @Test fun `insertPrivateShare inserts a private OCShare correctly`() { @@ -291,6 +292,34 @@ class OCShareRepositoryTest { } } + @Test + fun `removeGraphShare removes a share correctly`() { + every { + remoteShareDataSource.removeGraphShare( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId + ) + } returns Unit + + ocShareRepository.removeGraphShare( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId + ) + + verify(exactly = 1) { + remoteShareDataSource.removeGraphShare( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId + ) + } + } + @Test fun `refreshSharesFromNetwork refreshes shares correctly when the list of shares received is not empty`() { every { From 7b7a81127464ab8279befc3186d26755e090ad71 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Tue, 15 Sep 2026 12:05:04 +0200 Subject: [PATCH 5/6] chore: add calens file Signed-off-by: Jorge Aguado Recio --- changelog/unreleased/4974 | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/unreleased/4974 diff --git a/changelog/unreleased/4974 b/changelog/unreleased/4974 new file mode 100644 index 00000000000..8d24611072d --- /dev/null +++ b/changelog/unreleased/4974 @@ -0,0 +1,7 @@ +Enhancement: Remove a share over a file or a folder on an oCIS server + +A new option to remove a share over a file or a folder on an oCIS has been added. +It will be only visible for users with proper permissions. + +https://github.com/owncloud/android/issues/4940 +https://github.com/owncloud/android/pull/4974 From e0ffdeb379b662b5acfcba2245f99b5b6437119d Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Tue, 15 Sep 2026 14:30:46 +0200 Subject: [PATCH 6/6] refactor: update content description for remove share button Signed-off-by: Jorge Aguado Recio --- .../owncloud/android/presentation/sharing/GraphSharesAdapter.kt | 2 +- owncloudApp/src/main/res/values/strings.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt index 018e9516ec5..dcd3c476acc 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt @@ -62,7 +62,7 @@ class GraphSharesAdapter( memberRole.text = roleNames.joinToString(", ") removeMemberButton.apply { - contentDescription = holder.itemView.context.getString(R.string.content_description_remove_share_button, share.displayName) + contentDescription = holder.itemView.context.getString(R.string.content_description_delete_share, share.displayName) isVisible = canRemoveShares setOnClickListener { listener.onRemoveShare(share) diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index aa5220eb3b6..67a4fb0f41b 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -731,7 +731,6 @@ Remove password Generate password Copy password - Remove share %1$s Create a shortcut URL