From 75670ef9d64dff3c91069f133adf6378046b5449 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Tue, 15 Sep 2026 14:41:49 +0200 Subject: [PATCH 1/7] feat: show pencil 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 ++- 4 files changed, 61 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..cc0aa4b268a 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 canEditShares: 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, canEditShares) 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) { + canEditShares = DRIVES_UPDATE_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_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..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..3db3290b754 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 canEditShares = 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, 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) } 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 } From 44a8dbcd97253ffefe4a41bc1eee90df494a940a Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Fri, 18 Sep 2026 11:05:03 +0200 Subject: [PATCH 2/7] feat: show edit share screen with member and role binding Signed-off-by: Jorge Aguado Recio --- .../android/extensions/SpaceMemberExt.kt | 1 - .../sharing/AddGraphShareFragment.kt | 47 ++++++++++++++++--- .../sharing/GraphShareFragment.kt | 20 ++++++-- .../sharing/GraphSharesAdapter.kt | 11 ++++- .../presentation/sharing/ShareActivity.kt | 12 ++++- 5 files changed, 75 insertions(+), 16 deletions(-) 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..dc1aeac4ac2 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,7 @@ 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 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = AddMemberFragmentBinding.inflate(inflater, container, false) @@ -81,6 +83,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 +104,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 { @@ -142,11 +155,7 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap 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") } @@ -181,7 +190,7 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap 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 -> @@ -227,15 +236,39 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } + private fun bindEditMode(share: MemberPermission, roles: List) { + 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 cc0aa4b268a..c4f3376af46 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!! @@ -66,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 @@ -83,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) } } @@ -105,6 +106,15 @@ 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() @@ -118,7 +128,6 @@ class GraphShareFragment : Fragment() { when (val uiResult = event.peekContent()) { is UIResult.Success -> { uiResult.data?.let { - roles = it graphShareViewModel.getGraphShares() } } @@ -138,6 +147,7 @@ class GraphShareFragment : Fragment() { 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 @@ -194,7 +204,7 @@ class GraphShareFragment : Fragment() { } interface GraphShareFragmentListener { - fun addGraphShare(file: OCFile, accountName: String) + fun addGraphShare(file: OCFile, accountName: String, roles: List, editMode: Boolean, selectedShare: MemberPermission?) } companion object { 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 3db3290b754..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,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, 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) From 33619dbcbd34ba0d999d16e863c201f773331bb9 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Fri, 18 Sep 2026 13:25:38 +0200 Subject: [PATCH 3/7] 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/AddGraphShareFragment.kt | 18 +++- .../sharing/GraphShareFragment.kt | 16 ++++ .../sharing/GraphShareViewModel.kt | 29 ++++++ owncloudApp/src/main/res/values/strings.xml | 2 + .../shares/EditRemoteGraphShareOperation.kt | 89 +++++++++++++++++++ .../resources/shares/services/ShareService.kt | 8 ++ .../services/implementation/OCShareService.kt | 10 +++ .../datasources/RemoteShareDataSource.kt | 9 ++ .../implementation/OCRemoteShareDataSource.kt | 13 +++ .../shares/repository/OCShareRepository.kt | 9 ++ .../domain/sharing/shares/ShareRepository.kt | 9 ++ .../usecases/EditGraphShareAsyncUseCase.kt | 41 +++++++++ 13 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/EditRemoteGraphShareOperation.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/EditGraphShareAsyncUseCase.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..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/presentation/sharing/AddGraphShareFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/AddGraphShareFragment.kt index dc1aeac4ac2..70397d75784 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 @@ -70,6 +70,7 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap 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) @@ -218,7 +219,11 @@ 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) + } } } } @@ -234,9 +239,20 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } } + + 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] } 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 c4f3376af46..9b0f33ac32e 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 @@ -120,6 +120,7 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList observeShares() observeSpacePermissions() observeAddShareResult() + observeEditShareResult() } private fun observeRoles() { @@ -203,6 +204,21 @@ 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, roles: List, editMode: Boolean, selectedShare: MemberPermission?) } 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..f46a0359fe5 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 @@ -32,6 +32,7 @@ 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 @@ -51,6 +52,7 @@ import kotlinx.coroutines.launch class GraphShareViewModel( private val addGraphShareAsyncUseCase: AddGraphShareAsyncUseCase, + private val editGraphShareAsyncUseCase: EditGraphShareAsyncUseCase, private val getRolesAsyncUseCase: GetRolesAsyncUseCase, private val getGraphSharesAsyncUseCase: GetGraphSharesAsyncUseCase, private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase, @@ -80,6 +82,9 @@ 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 @@ -167,6 +172,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) { @@ -200,6 +228,7 @@ class GraphShareViewModel( fun resetViewModel() { _addShareUIState.value = null _addShareResultFlow.value = null + _editShareResultFlow.value = null } data class MembersUIState( 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/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? + ) +} From 88fd61bdbdbd6a27e03aa3ca9add07858fbddf00 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Fri, 18 Sep 2026 13:32:14 +0200 Subject: [PATCH 4/7] refactor: remove unnecessary roles loading Signed-off-by: Jorge Aguado Recio --- .../sharing/GraphShareFragment.kt | 20 ------------------- .../sharing/GraphShareViewModel.kt | 12 +---------- 2 files changed, 1 insertion(+), 31 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 9b0f33ac32e..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 @@ -116,32 +116,12 @@ class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterList } private fun subscribeToViewModels() { - observeRoles() observeShares() observeSpacePermissions() observeAddShareResult() observeEditShareResult() } - private fun observeRoles() { - collectLatestLifecycleFlow(graphShareViewModel.roles) { event -> - event?.let { - when (val uiResult = event.peekContent()) { - is UIResult.Success -> { - uiResult.data?.let { - 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") - } - } - } - } - } - private fun observeShares() { collectLatestLifecycleFlow(graphShareViewModel.shares) { event -> event?.let { 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 f46a0359fe5..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,7 +30,6 @@ 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 @@ -53,7 +52,6 @@ import kotlinx.coroutines.launch class GraphShareViewModel( private val addGraphShareAsyncUseCase: AddGraphShareAsyncUseCase, private val editGraphShareAsyncUseCase: EditGraphShareAsyncUseCase, - private val getRolesAsyncUseCase: GetRolesAsyncUseCase, private val getGraphSharesAsyncUseCase: GetGraphSharesAsyncUseCase, private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase, private val searchMembersUseCase: SearchMembersUseCase, @@ -64,9 +62,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 @@ -92,12 +87,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 +97,7 @@ class GraphShareViewModel( viewModelScope.launch(coroutineDispatcherProvider.io) { capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName)) } + getGraphShares() getSpacePermissions() } From 4b0d0148544227e09e56b8dfc5965f8f60b6c15f Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Fri, 18 Sep 2026 13:37:37 +0200 Subject: [PATCH 5/7] refactor: split subscribeToViewModels into per-flow observe methods Signed-off-by: Jorge Aguado Recio --- .../sharing/AddGraphShareFragment.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 70397d75784..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 @@ -138,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()) { @@ -151,7 +160,9 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } } + } + private fun observeShares() { collectLatestLifecycleFlow(graphShareViewModel.shares) { event -> event?.let { when (val uiResult = event.peekContent()) { @@ -163,7 +174,9 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } } + } + private fun observeMembers() { collectLatestLifecycleFlow(graphShareViewModel.members) { uiState -> if (uiState.isLoading) { binding.indeterminateProgressBar.visibility = View.VISIBLE @@ -184,7 +197,9 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } } + } + private fun observeAddShareUIState() { collectLatestLifecycleFlow(graphShareViewModel.addShareUIState) { uiState -> uiState?.let { binding.apply { @@ -229,7 +244,9 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } } + } + private fun observeAddShareResult() { collectLatestLifecycleFlow(graphShareViewModel.addShareResultFlow) { event -> event?.peekContent()?.let { uiResult -> when (uiResult) { @@ -239,7 +256,9 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap } } } + } + private fun observeEditShareResult() { collectLatestLifecycleFlow(graphShareViewModel.editShareResultFlow) { event -> event?.peekContent()?.let { uiResult -> when (uiResult) { From d3d710d357e067b53f3f3b6b61b3def9e45c91b4 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Fri, 18 Sep 2026 13:48:49 +0200 Subject: [PATCH 6/7] test: create new tests for OCShareRepositoryTest and OCRemoteShareDataSourceTest Signed-off-by: Jorge Aguado Recio --- .../OCRemoteShareDataSourceTest.kt | 36 +++++++++++++++++++ .../repository/OCShareRepositoryTest.kt | 35 ++++++++++++++++++ 2 files changed, 71 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..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 { From 6efa09c228192ea57f01627455bd2d6ac14dd7a1 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Fri, 18 Sep 2026 13:55:35 +0200 Subject: [PATCH 7/7] chore: add calens file Signed-off-by: Jorge Aguado Recio --- changelog/unreleased/4978 | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/unreleased/4978 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