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 94e509739fc..a06647475cc 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 @@ -234,6 +235,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 62e9f23f9a9..d8b4288635a 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 @@ -32,6 +32,7 @@ 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.collectLatestLifecycleFlow import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar @@ -40,7 +41,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!! @@ -55,6 +56,7 @@ class GraphShareFragment : Fragment() { private var roles: List = emptyList() private var listener: GraphShareFragmentListener? = null + private var canEditShares: Boolean = false override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = MembersFragmentBinding.inflate(inflater, container, false) @@ -65,7 +67,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 @@ -82,7 +84,7 @@ class GraphShareFragment : Fragment() { 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) } } @@ -104,56 +106,69 @@ class GraphShareFragment : Fragment() { _binding = null } + 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() + observeEditShareResult() } - private fun observeRoles() { - collectLatestLifecycleFlow(graphShareViewModel.roles) { event -> + private fun observeShares() { + collectLatestLifecycleFlow(graphShareViewModel.shares) { event -> event?.let { when (val uiResult = event.peekContent()) { is UIResult.Success -> { uiResult.data?.let { - roles = it - graphShareViewModel.getGraphShares() + roles = it.roles + val hasMembers = it.members.isNotEmpty() + binding.membersRecyclerView.isVisible = hasMembers + binding.noSharesMessage.isVisible = !hasMembers + graphSharesAdapter.setShares(it.members, it.roles, canEditShares) + binding.swipeRefreshMembers.isRefreshing = false } } - is UIResult.Loading -> { } + is UIResult.Loading -> { binding.swipeRefreshMembers.isRefreshing = true } is UIResult.Error -> { + binding.swipeRefreshMembers.isRefreshing = false showErrorInSnackbar(R.string.share_sync_failed, uiResult.error) - Timber.e(uiResult.error, "Failed to retrieve platform roles") + Timber.e(uiResult.error, "Failed to retrieve shares") } } } } } - private fun observeShares() { - collectLatestLifecycleFlow(graphShareViewModel.shares) { event -> + private fun observeSpacePermissions() { + collectLatestLifecycleFlow(graphShareViewModel.spacePermissions) { event -> event?.let { when (val uiResult = event.peekContent()) { is UIResult.Success -> { - uiResult.data?.let { - val hasMembers = it.members.isNotEmpty() - binding.membersRecyclerView.isVisible = hasMembers - binding.noSharesMessage.isVisible = !hasMembers - graphSharesAdapter.setShares(it.members, it.roles) - binding.swipeRefreshMembers.isRefreshing = false + uiResult.data?.let { spacePermissions -> + checkPermissions(spacePermissions) } } - is UIResult.Loading -> { binding.swipeRefreshMembers.isRefreshing = true } + is UIResult.Loading -> { } is UIResult.Error -> { - binding.swipeRefreshMembers.isRefreshing = false - showErrorInSnackbar(R.string.share_sync_failed, uiResult.error) - Timber.e(uiResult.error, "Failed to retrieve shares") + Timber.e(uiResult.error, "Failed to retrieve space permissions") } } } } } + private fun checkPermissions(spacePermissions: List) { + canEditShares = DRIVES_UPDATE_PERMISSION in spacePermissions + } + private fun observeAddShareResult() { collectLatestLifecycleFlow(graphShareViewModel.addShareResultFlow) { event -> event?.peekContent()?.let { uiResult -> @@ -169,13 +184,29 @@ class GraphShareFragment : Fragment() { } } + 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_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 4b683896144..81d10bf925e 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,11 +30,12 @@ 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.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 @@ -50,19 +51,17 @@ 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, private val getUserIdAsyncUseCase: GetUserIdAsyncUseCase, + private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase, private val accountName: String, private val file: OCFile, private val coroutineDispatcherProvider: CoroutinesDispatcherProvider, ) : ViewModel() { - private val _roles = MutableStateFlow>>?>(null) - val roles: StateFlow>>?> = _roles - private val _shares = MutableStateFlow>?>(null) val shares: StateFlow>?> = _shares @@ -78,16 +77,16 @@ class GraphShareViewModel( private val _addShareResultFlow = MutableStateFlow>?>(null) val addShareResultFlow: StateFlow>?> = _addShareResultFlow + private val _editShareResultFlow = MutableStateFlow>?>(null) + val editShareResultFlow: StateFlow>?> = _editShareResultFlow + private var searchJob: Job? = null var capabilities: OCCapability? = null + private val _spacePermissions = MutableStateFlow>>?>(null) + val spacePermissions: StateFlow>>?> = _spacePermissions + init { - runUseCaseWithResult( - coroutineDispatcher = coroutineDispatcherProvider.io, - flow = _roles, - useCase = getRolesAsyncUseCase, - useCaseParams = GetRolesAsyncUseCase.Params(accountName = accountName), - ) runUseCaseWithResult( coroutineDispatcher = coroutineDispatcherProvider.io, showLoading = false, @@ -98,6 +97,25 @@ class GraphShareViewModel( viewModelScope.launch(coroutineDispatcherProvider.io) { capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName)) } + getGraphShares() + 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() { @@ -144,6 +162,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) { @@ -177,6 +218,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 bc765fe678c..0e81386e5ec 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,10 +33,13 @@ 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() + private var canEditShares = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphShareViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -58,6 +61,14 @@ class GraphSharesAdapter : RecyclerView.Adapter, roles: List) { + fun setShares(shares: List, roles: List, canEditShares: Boolean) { + val hasUserPermissionsChanged = this.canEditShares != canEditShares + this.canEditShares = canEditShares 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) } @@ -85,4 +98,8 @@ class GraphSharesAdapter : RecyclerView.Adapter, 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/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 1e3d2b988c6..0f9d7c5de04 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 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 e9a4f0c4455..b6a753d4c4f 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,14 @@ interface ShareService : Service { expirationDate: 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 f22bdc472e3..a654676f3ac 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.RemoveRemoteShareOperation @@ -63,6 +64,15 @@ class OCShareService(override val client: OwnCloudClient) : ShareService { ): RemoteOperationResult = AddRemoteGraphShareOperation(spaceId, itemId, memberId, memberType, roleId, expirationDate).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 eb9d4178aa4..d681afa3197 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,15 @@ interface RemoteShareDataSource { expirationDate: 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 ddb0fa5fe78..c6ff97a731a 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,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 f09f19a33c8..45bf81df9a9 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,15 @@ class OCShareRepository( expirationDate: String? ) = remoteShareDataSource.addGraphShare(accountName, spaceId, itemId, member, roleId, expirationDate) + 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 ac5f7b28dec..6b8aacfdc23 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,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 12063015db5..725888958be 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,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 6f70df52236..e2d9063da4b 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,15 @@ interface ShareRepository { expirationDate: 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? + ) +}