diff --git a/changelog/unreleased/4978 b/changelog/unreleased/4978 new file mode 100644 index 00000000000..3a4dd9072ff --- /dev/null +++ b/changelog/unreleased/4978 @@ -0,0 +1,7 @@ +Enhancement: Edit a share over a file or a folder on an oCIS server + +A new option to edit 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/4937 +https://github.com/owncloud/android/pull/4978 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 fb6bb8f7cfd..bfc43d9ac4a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -94,6 +94,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.AddGraphShareAsyncUse import com.owncloud.android.domain.sharing.shares.usecases.CreatePrivateShareAsyncUseCase import com.owncloud.android.domain.sharing.shares.usecases.CreatePublicShareAsyncUseCase import com.owncloud.android.domain.sharing.shares.usecases.DeleteShareAsyncUseCase +import com.owncloud.android.domain.sharing.shares.usecases.EditGraphShareAsyncUseCase import com.owncloud.android.domain.sharing.shares.usecases.EditPrivateShareAsyncUseCase import com.owncloud.android.domain.sharing.shares.usecases.EditPublicShareAsyncUseCase import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUseCase @@ -235,6 +236,7 @@ val useCaseModule = module { factoryOf(::CreatePrivateShareAsyncUseCase) factoryOf(::CreatePublicShareAsyncUseCase) factoryOf(::DeleteShareAsyncUseCase) + factoryOf(::EditGraphShareAsyncUseCase) factoryOf(::EditPrivateShareAsyncUseCase) factoryOf(::EditPublicShareAsyncUseCase) factoryOf(::GetGraphSharesAsyncUseCase) diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt index 79e2954a43f..670d73eabfb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt @@ -28,7 +28,6 @@ private const val GROUP_PREFIX = "g:" private const val USER_PREFIX = "u:" fun MemberPermission.toOCMember(): OCMember { - val isGroup = id.startsWith(GROUP_PREFIX) val type = if (isGroup) OCMemberType.GROUP else OCMemberType.USER return OCMember( id = id.removePrefix(if (isGroup) GROUP_PREFIX else USER_PREFIX), diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/AddGraphShareFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/AddGraphShareFragment.kt index 272f327714f..daa0dc5d97d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/AddGraphShareFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/AddGraphShareFragment.kt @@ -42,6 +42,7 @@ import com.owncloud.android.extensions.collectLatestLifecycleFlow import com.owncloud.android.extensions.openDatePickerDialog import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showOrHideEmptyView +import com.owncloud.android.extensions.toOCMember import com.owncloud.android.presentation.spaces.members.SearchMembersAdapter import com.owncloud.android.presentation.spaces.members.SpaceRolesAdapter import com.owncloud.android.utils.DisplayUtils @@ -68,6 +69,8 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap private var currentShares: List = emptyList() private var searchMinLength = DEFAULT_SEARCH_MIN_LENGTH private var currentUserId: String? = null + private var editMode = false + private var selectedShareId = "" override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = AddMemberFragmentBinding.inflate(inflater, container, false) @@ -81,6 +84,10 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + editMode = requireArguments().getBoolean(ARG_EDIT_MODE, false) + roles = requireArguments().getParcelableArrayList(ARG_ROLES) ?: emptyList() + searchMembersAdapter = SearchMembersAdapter(this) recyclerView = binding.membersRecyclerView recyclerView.apply { @@ -98,6 +105,13 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } rolesAdapter.setRoles(roles) + if (editMode) { + val selectedShare = requireArguments().getParcelable(ARG_SELECTED_SHARE) + selectedShare?.let { + bindEditMode(it, roles) + } + } + subscribeToViewModels() binding.searchBar.apply { @@ -124,6 +138,15 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap private fun subscribeToViewModels() { searchMinLength = graphShareViewModel.capabilities?.filesSharingSearchMinLength ?: DEFAULT_SEARCH_MIN_LENGTH + observeUserId() + observeShares() + observeMembers() + observeAddShareUIState() + observeAddShareResult() + observeEditShareResult() + } + + private fun observeUserId() { collectLatestLifecycleFlow(graphShareViewModel.userId) { event -> event?.let { when (val uiResult = event.peekContent()) { @@ -137,23 +160,23 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } } + } + private fun observeShares() { collectLatestLifecycleFlow(graphShareViewModel.shares) { event -> event?.let { when (val uiResult = event.peekContent()) { is UIResult.Success -> { - uiResult.data?.let { - roles = it.roles - currentShares = it.members - rolesAdapter.setRoles(roles) - } + uiResult.data?.let { currentShares = it.members } } is UIResult.Loading -> { } is UIResult.Error -> { Timber.e(uiResult.error, "Failed to retrieve shares") } } } } + } + private fun observeMembers() { collectLatestLifecycleFlow(graphShareViewModel.members) { uiState -> if (uiState.isLoading) { binding.indeterminateProgressBar.visibility = View.VISIBLE @@ -174,14 +197,16 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } } + } + private fun observeAddShareUIState() { collectLatestLifecycleFlow(graphShareViewModel.addShareUIState) { uiState -> uiState?.let { binding.apply { searchMemberLayout.visibility = View.GONE addMemberLayout.visibility = View.VISIBLE inviteMemberButton.visibility = View.VISIBLE - inviteMemberButton.text = getString(R.string.action_share) + inviteMemberButton.text = getString(if (editMode) R.string.share_confirm_public_link_button else R.string.action_share) inviteMemberButton.contentDescription = getString(R.string.content_description_create_share_button) } it.selectedMember?.let { member -> @@ -209,13 +234,19 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap binding.inviteMemberButton.setOnClickListener { uiState.selectedMember?.let { selectedMember -> uiState.selectedRole?.let { selectedRole -> - graphShareViewModel.addGraphShare(selectedMember, selectedRole.id) + if (editMode) { + graphShareViewModel.editGraphShare(selectedShareId, selectedRole.id, uiState.selectedExpirationDate) + } else { + graphShareViewModel.addGraphShare(selectedMember, selectedRole.id) + } } } } } } + } + private fun observeAddShareResult() { collectLatestLifecycleFlow(graphShareViewModel.addShareResultFlow) { event -> event?.peekContent()?.let { uiResult -> when (uiResult) { @@ -227,15 +258,52 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } + private fun observeEditShareResult() { + collectLatestLifecycleFlow(graphShareViewModel.editShareResultFlow) { event -> + event?.peekContent()?.let { uiResult -> + when (uiResult) { + is UIResult.Loading -> { } + is UIResult.Success -> parentFragmentManager.popBackStack() + is UIResult.Error -> showErrorInSnackbar(R.string.share_edit_failed, uiResult.error) + } + } + } + } + + private fun bindEditMode(share: MemberPermission, roles: List) { + selectedShareId = share.id + graphShareViewModel.onMemberSelected(share.toOCMember()) + + val selectedRole = roles.first { it.id == share.roles[0] } + graphShareViewModel.onRoleSelected(selectedRole) + + share.expirationDateTime?.let { expirationDate -> + graphShareViewModel.onExpirationDateSelected(expirationDate) + binding.expirationDateLayout.expirationDateSwitch.isChecked = true + } + } + companion object { private const val ARG_FILE = "FILE" private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME" + private const val ARG_EDIT_MODE = "EDIT_MODE" + private const val ARG_SELECTED_SHARE = "SELECTED_SHARE" + private const val ARG_ROLES = "ROLES" private const val DEFAULT_SEARCH_MIN_LENGTH = 3 - fun newInstance(file: OCFile, accountName: String): AddGraphShareFragment { + fun newInstance( + file: OCFile, + accountName: String, + roles: List, + editMode: Boolean, + selectedShare: MemberPermission? + ): AddGraphShareFragment { val args = Bundle().apply { putParcelable(ARG_FILE, file) putString(ARG_ACCOUNT_NAME, accountName) + putParcelableArrayList(ARG_ROLES, ArrayList(roles)) + putBoolean(ARG_EDIT_MODE, editMode) + putParcelable(ARG_SELECTED_SHARE, selectedShare) } return AddGraphShareFragment().apply { arguments = args 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 b24422b8a4b..1a9634087c2 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 @@ -59,6 +59,7 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList private var roles: List = emptyList() private var listener: GraphShareFragmentListener? = null private var canRemoveShares: Boolean = false + private var canEditShares: Boolean = false override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = MembersFragmentBinding.inflate(inflater, container, false) @@ -86,7 +87,7 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList binding.addMemberButton.setOnClickListener { if (file != null && accountName != null) { graphShareViewModel.resetViewModel() - listener?.addGraphShare(file = file, accountName = accountName) + listener?.addGraphShare(file = file, accountName = accountName, roles = roles, editMode = false, selectedShare = null) } } @@ -119,32 +120,21 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList .avoidScreenshotsIfNeeded() } + override fun onEditShare(share: MemberPermission) { + val file = requireArguments().getParcelable(ARG_FILE) + val accountName = requireArguments().getString(ARG_ACCOUNT_NAME) + if (file != null && accountName != null) { + graphShareViewModel.resetViewModel() + listener?.addGraphShare(file = file, accountName = accountName, roles = roles, editMode = true, selectedShare = share) + } + } + private fun subscribeToViewModels() { - observeRoles() observeShares() observeSpacePermissions() observeAddShareResult() observeRemoveShareResult() - } - - private fun observeRoles() { - collectLatestLifecycleFlow(graphShareViewModel.roles) { event -> - event?.let { - when (val uiResult = event.peekContent()) { - is UIResult.Success -> { - uiResult.data?.let { - roles = it - graphShareViewModel.getGraphShares() - } - } - is UIResult.Loading -> { } - is UIResult.Error -> { - showErrorInSnackbar(R.string.share_sync_failed, uiResult.error) - Timber.e(uiResult.error, "Failed to retrieve platform roles") - } - } - } - } + observeEditShareResult() } private fun observeShares() { @@ -153,10 +143,11 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList when (val uiResult = event.peekContent()) { is UIResult.Success -> { uiResult.data?.let { + roles = it.roles val hasMembers = it.members.isNotEmpty() binding.membersRecyclerView.isVisible = hasMembers binding.noSharesMessage.isVisible = !hasMembers - graphSharesAdapter.setShares(it.members, it.roles, canRemoveShares) + graphSharesAdapter.setShares(it.members, it.roles, canRemoveShares, canEditShares) binding.swipeRefreshMembers.isRefreshing = false } } @@ -191,6 +182,7 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList private fun checkPermissions(spacePermissions: List) { canRemoveShares = DRIVES_DELETE_PERMISSION in spacePermissions + canEditShares = DRIVES_UPDATE_PERMISSION in spacePermissions } private fun observeAddShareResult() { @@ -224,14 +216,30 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList } } + private fun observeEditShareResult() { + collectLatestLifecycleFlow(graphShareViewModel.editShareResultFlow) { event -> + event?.peekContent()?.let { uiResult -> + when (uiResult) { + is UIResult.Loading -> { } + is UIResult.Success -> { + showMessageInSnackbar(getString(R.string.share_edit_correctly)) + graphShareViewModel.resetViewModel() + } + is UIResult.Error -> { } + } + } + } + } + interface GraphShareFragmentListener { - fun addGraphShare(file: OCFile, accountName: String) + fun addGraphShare(file: OCFile, accountName: String, roles: List, editMode: Boolean, selectedShare: MemberPermission?) } 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" + private const val DRIVES_UPDATE_PERMISSION = "libre.graph/driveItem/permissions/update" 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 40b6c958d54..4b5374a58e6 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 @@ -30,8 +30,8 @@ import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.members.model.OCMember import com.owncloud.android.domain.members.usecases.SearchMembersUseCase 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.EditGraphShareAsyncUseCase 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 @@ -52,7 +52,7 @@ import kotlinx.coroutines.launch class GraphShareViewModel( private val addGraphShareAsyncUseCase: AddGraphShareAsyncUseCase, - private val getRolesAsyncUseCase: GetRolesAsyncUseCase, + private val editGraphShareAsyncUseCase: EditGraphShareAsyncUseCase, private val getGraphSharesAsyncUseCase: GetGraphSharesAsyncUseCase, private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase, private val searchMembersUseCase: SearchMembersUseCase, @@ -64,9 +64,6 @@ class GraphShareViewModel( private val coroutineDispatcherProvider: CoroutinesDispatcherProvider, ) : ViewModel() { - private val _roles = MutableStateFlow>>?>(null) - val roles: StateFlow>>?> = _roles - private val _shares = MutableStateFlow>?>(null) val shares: StateFlow>?> = _shares @@ -85,6 +82,9 @@ class GraphShareViewModel( private val _removeShareResultFlow = MutableSharedFlow>() val removeShareResultFlow: SharedFlow> = _removeShareResultFlow + private val _editShareResultFlow = MutableStateFlow>?>(null) + val editShareResultFlow: StateFlow>?> = _editShareResultFlow + private var searchJob: Job? = null var capabilities: OCCapability? = null @@ -92,12 +92,6 @@ class GraphShareViewModel( val spacePermissions: StateFlow>>?> = _spacePermissions init { - runUseCaseWithResult( - coroutineDispatcher = coroutineDispatcherProvider.io, - flow = _roles, - useCase = getRolesAsyncUseCase, - useCaseParams = GetRolesAsyncUseCase.Params(accountName = accountName), - ) runUseCaseWithResult( coroutineDispatcher = coroutineDispatcherProvider.io, showLoading = false, @@ -108,6 +102,7 @@ class GraphShareViewModel( viewModelScope.launch(coroutineDispatcherProvider.io) { capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName)) } + getGraphShares() getSpacePermissions() } @@ -195,6 +190,29 @@ class GraphShareViewModel( ) } + fun editGraphShare(shareId: String, roleId: String, expirationDate: String?) { + val spaceId = file.spaceId + val itemId = file.remoteId + if (spaceId == null || itemId == null) { + _editShareResultFlow.update { Event(UIResult.Error(error = IncompleteFileDataException())) } + return + } + + runUseCaseWithResult( + coroutineDispatcher = coroutineDispatcherProvider.io, + flow = _editShareResultFlow, + useCase = editGraphShareAsyncUseCase, + useCaseParams = EditGraphShareAsyncUseCase.Params( + accountName = accountName, + spaceId = spaceId, + itemId = itemId, + shareId = shareId, + roleId = roleId, + expirationDate = expirationDate + ) + ) + } + fun searchMembers(query: String) { searchJob?.cancel() searchJob = viewModelScope.launch(coroutineDispatcherProvider.io) { @@ -228,6 +246,7 @@ class GraphShareViewModel( fun resetViewModel() { _addShareUIState.value = null _addShareResultFlow.value = null + _editShareResultFlow.value = null } data class MembersUIState( 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 dcd3c476acc..fc17998f77f 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 @@ -40,6 +40,7 @@ class GraphSharesAdapter( private var shares: List = emptyList() private var rolesMap: Map = emptyMap() private var canRemoveShares = false + private var canEditShares = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphShareViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -69,6 +70,14 @@ class GraphSharesAdapter( } } + editMemberButton.apply { + contentDescription = holder.itemView.context.getString(R.string.content_description_edit_share, share.displayName) + isVisible = canEditShares + setOnClickListener { + listener.onEditShare(share) + } + } + val hasExpirationDate = share.expirationDateTime != null expirationCalendarIcon.isVisible = hasExpirationDate expirationDate.isVisible = hasExpirationDate @@ -82,9 +91,10 @@ class GraphSharesAdapter( override fun getItemCount(): Int = shares.size - fun setShares(shares: List, roles: List, canRemoveShares: Boolean) { - val hasUserPermissionsChanged = this.canRemoveShares != canRemoveShares + fun setShares(shares: List, roles: List, canRemoveShares: Boolean, canEditShares: Boolean) { + val hasUserPermissionsChanged = this.canRemoveShares != canRemoveShares || this.canEditShares != canEditShares this.canRemoveShares = canRemoveShares + this.canEditShares = canEditShares this.rolesMap = roles.associate { it.id to it.displayName } val sortedShares = shares.sortedWith( compareBy { it.isGroup } @@ -101,5 +111,6 @@ class GraphSharesAdapter( interface GraphSharesAdapterListener { fun onRemoveShare(share: MemberPermission) + fun onEditShare(share: MemberPermission) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt index cf66eed3b8c..fefce88c6ef 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt @@ -40,6 +40,8 @@ import com.owncloud.android.R import com.owncloud.android.databinding.MembersActivityBinding import com.owncloud.android.datamodel.ThumbnailsCacheManager 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.domain.sharing.shares.model.OCShare import com.owncloud.android.domain.sharing.shares.model.ShareType import com.owncloud.android.domain.utils.Event.EventObserver @@ -110,8 +112,14 @@ class ShareActivity : FileActivity(), ShareFragmentListener, GraphShareFragment. } } - override fun addGraphShare(file: OCFile, accountName: String) { - val addGraphShareFragment = AddGraphShareFragment.newInstance(file, accountName) + override fun addGraphShare(file: OCFile, accountName: String, roles: List, editMode: Boolean, selectedShare: MemberPermission?) { + val addGraphShareFragment = AddGraphShareFragment.newInstance( + file = file, + accountName = accountName, + roles = roles, + editMode = editMode, + selectedShare = selectedShare + ) val transaction = supportFragmentManager.beginTransaction() transaction.apply { replace(R.id.members_fragment_container, addGraphShareFragment, TAG_ADD_GRAPH_SHARE_FRAGMENT) diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 7136e10288d..5eff3362261 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -499,6 +499,8 @@ Is already shared with this user/group Share removed correctly Share could not be removed + Share edited correctly + Share could not be edited Public links Create link share Edit link share diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/EditRemoteGraphShareOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/EditRemoteGraphShareOperation.kt new file mode 100644 index 00000000000..d012fc5bb03 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/EditRemoteGraphShareOperation.kt @@ -0,0 +1,89 @@ +/** + * 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.HttpConstants.CONTENT_TYPE_JSON +import com.owncloud.android.lib.common.http.methods.nonwebdav.PatchMethod +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 okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray +import org.json.JSONObject +import timber.log.Timber +import java.net.URL + +class EditRemoteGraphShareOperation( + private val spaceId: String, + private val itemId: String, + private val shareId: String, + private val roleId: String, + private val expirationDate: 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 requestBody = JSONObject().apply { + put(EXPIRATION_DATE_BODY_PARAM, expirationDate ?: JSONObject.NULL) + put(ROLES_BODY_PARAM, JSONArray().apply { put(roleId) }) + }.toString().toRequestBody(CONTENT_TYPE_JSON.toMediaType()) + + val patchMethod = PatchMethod(URL(uriBuilder.build().toString()), requestBody) + + val status = client.executeHttpMethod(patchMethod) + + val response = patchMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_OK) { + Timber.d("Successful response: $response") + result = RemoteOperationResult(ResultCode.OK) + Timber.d("Edit graph share operation completed") + } else { + result = RemoteOperationResult(patchMethod) + Timber.e("Failed response while editing a graph share; status code: $status, response: $response") + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Timber.e(e, "Exception while editing 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" + private const val EXPIRATION_DATE_BODY_PARAM = "expirationDateTime" + private const val ROLES_BODY_PARAM = "roles" + } +} 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 0c1b64911ac..fd4fedbd078 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 @@ -56,6 +56,14 @@ interface ShareService : Service { shareId: String ): RemoteOperationResult + fun editGraphShare( + spaceId: String, + itemId: String, + shareId: String, + roleId: String, + expirationDate: 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 06bbc78863a..1df26b10af4 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 @@ -30,6 +30,7 @@ import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.shares.AddRemoteGraphShareOperation import com.owncloud.android.lib.resources.shares.CreateRemoteShareOperation +import com.owncloud.android.lib.resources.shares.EditRemoteGraphShareOperation import com.owncloud.android.lib.resources.shares.GetRemoteGraphSharesForFileOperation import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation import com.owncloud.android.lib.resources.shares.RemoveRemoteGraphShareOperation @@ -71,6 +72,15 @@ class OCShareService(override val client: OwnCloudClient) : ShareService { ): RemoteOperationResult = RemoveRemoteGraphShareOperation(spaceId, itemId, shareId).execute(client) + override fun editGraphShare( + spaceId: String, + itemId: String, + shareId: String, + roleId: String, + expirationDate: String? + ): RemoteOperationResult = + EditRemoteGraphShareOperation(spaceId, itemId, shareId, roleId, expirationDate).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 0c217862d2c..5f51e84342e 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 @@ -57,6 +57,15 @@ interface RemoteShareDataSource { shareId: String ) + fun editGraphShare( + accountName: String, + spaceId: String, + itemId: String, + shareId: String, + roleId: String, + expirationDate: 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 3e0915292c9..3754ef0db96 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 @@ -94,6 +94,19 @@ class OCRemoteShareDataSource( } } + override fun editGraphShare( + accountName: String, + spaceId: String, + itemId: String, + shareId: String, + roleId: String, + expirationDate: String? + ) { + executeRemoteOperation { + clientManager.getShareService(accountName).editGraphShare(spaceId, itemId, shareId, roleId, expirationDate) + } + } + 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 603a92ba36f..7429e9d7662 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 @@ -140,6 +140,15 @@ class OCShareRepository( shareId: String ) = remoteShareDataSource.removeGraphShare(accountName, spaceId, itemId, shareId) + override fun editGraphShare( + accountName: String, + spaceId: String, + itemId: String, + shareId: String, + roleId: String, + expirationDate: String? + ) = remoteShareDataSource.editGraphShare(accountName, spaceId, itemId, shareId, roleId, expirationDate) + override fun refreshSharesFromNetwork( filePath: String, accountName: String 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 7849c89784e..696f133f312 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 @@ -480,6 +480,41 @@ class OCRemoteShareDataSourceTest { } } + @Test + fun `editGraphShare edits a graph share correctly`() { + val editGraphShareResult = createRemoteOperationResultMock(Unit, isSuccess = true) + + every { + ocShareService.editGraphShare( + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId, + roleId = SPACE_MEMBERS.roles[0].id, + expirationDate = null + ) + } returns editGraphShareResult + + ocRemoteShareDataSource.editGraphShare( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId, + roleId = SPACE_MEMBERS.roles[0].id, + expirationDate = null + ) + + verify(exactly = 1) { + clientManager.getShareService(OC_ACCOUNT_NAME) + ocShareService.editGraphShare( + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId, + roleId = SPACE_MEMBERS.roles[0].id, + expirationDate = null + ) + } + } + @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 a3ef043c4a5..48d29024f42 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 @@ -320,6 +320,40 @@ class OCShareRepositoryTest { } } + @Test + fun `editGraphShare edits a share correctly`() { + every { + remoteShareDataSource.editGraphShare( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId, + roleId = SPACE_MEMBERS.roles[0].id, + expirationDate = null + ) + } returns Unit + + ocShareRepository.editGraphShare( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId, + roleId = SPACE_MEMBERS.roles[0].id, + expirationDate = null + ) + + verify(exactly = 1) { + remoteShareDataSource.editGraphShare( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + itemId = OC_FILE.remoteId.orEmpty(), + shareId = shareId, + roleId = SPACE_MEMBERS.roles[0].id, + expirationDate = null + ) + } + } + @Test fun `refreshSharesFromNetwork refreshes shares correctly when the list of shares received is not empty`() { every { 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 4842e603e62..ce82324202c 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 @@ -100,6 +100,15 @@ interface ShareRepository { shareId: String ) + fun editGraphShare( + accountName: String, + spaceId: String, + itemId: String, + shareId: String, + roleId: String, + expirationDate: 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/EditGraphShareAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/EditGraphShareAsyncUseCase.kt new file mode 100644 index 00000000000..e071ddf2939 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/EditGraphShareAsyncUseCase.kt @@ -0,0 +1,41 @@ +/** + * 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 EditGraphShareAsyncUseCase( + private val shareRepository: ShareRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Params) = + shareRepository.editGraphShare(params.accountName, params.spaceId, params.itemId, params.shareId, params.roleId, params.expirationDate) + + data class Params( + val accountName: String, + val spaceId: String, + val itemId: String, + val shareId: String, + val roleId: String, + val expirationDate: String? + ) +}