diff --git a/changelog/unreleased/4945 b/changelog/unreleased/4945 new file mode 100644 index 00000000000..1a651433576 --- /dev/null +++ b/changelog/unreleased/4945 @@ -0,0 +1,7 @@ +Enhancement: List shares for oCIS accounts + +A new UI has been implemented to list and show information about the +shares of a file or folder on oCIS accounts (using Graph API) + +https://github.com/owncloud/android/issues/4876 +https://github.com/owncloud/android/pull/4945 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 8732933e9f5..8b1eae2e9e5 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -95,6 +95,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.CreatePublicShareAsyn import com.owncloud.android.domain.sharing.shares.usecases.DeleteShareAsyncUseCase 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 import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase @@ -233,6 +234,7 @@ val useCaseModule = module { factoryOf(::DeleteShareAsyncUseCase) factoryOf(::EditPrivateShareAsyncUseCase) factoryOf(::EditPublicShareAsyncUseCase) + factoryOf(::GetGraphSharesAsyncUseCase) factoryOf(::GetShareAsLiveDataUseCase) factoryOf(::GetShareesAsyncUseCase) factoryOf(::GetSharesAsLiveDataUseCase) diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index c9dee872bcb..c22b569ea3d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -53,6 +53,7 @@ import com.owncloud.android.presentation.settings.automaticuploads.SettingsVideo import com.owncloud.android.presentation.settings.logging.SettingsLogsViewModel import com.owncloud.android.presentation.settings.more.SettingsMoreViewModel import com.owncloud.android.presentation.settings.security.SettingsSecurityViewModel +import com.owncloud.android.presentation.sharing.GraphShareViewModel import com.owncloud.android.presentation.sharing.ShareViewModel import com.owncloud.android.presentation.spaces.SpacesListViewModel import com.owncloud.android.presentation.spaces.links.SpaceLinksViewModel @@ -70,6 +71,7 @@ val viewModelModule = module { viewModelOf(::DrawerViewModel) viewModelOf(::FileDetailsViewModel) viewModelOf(::FileOperationsViewModel) + viewModelOf(::GraphShareViewModel) viewModelOf(::LogListViewModel) viewModelOf(::OAuthViewModel) viewModelOf(::PatternViewModel) 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 93619de7848..79e2954a43f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt @@ -22,12 +22,12 @@ package com.owncloud.android.extensions import com.owncloud.android.domain.members.model.OCMember import com.owncloud.android.domain.members.model.OCMemberType -import com.owncloud.android.domain.spaces.model.SpaceMember +import com.owncloud.android.domain.sharing.shares.model.MemberPermission private const val GROUP_PREFIX = "g:" private const val USER_PREFIX = "u:" -fun SpaceMember.toOCMember(): OCMember { +fun MemberPermission.toOCMember(): OCMember { val isGroup = id.startsWith(GROUP_PREFIX) val type = if (isGroup) OCMemberType.GROUP else OCMemberType.USER return OCMember( diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt index d79a4f68baa..5b7dd781266 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt @@ -33,6 +33,7 @@ import com.owncloud.android.domain.exceptions.CopyIntoSameFolderException import com.owncloud.android.domain.exceptions.FileAlreadyExistsException import com.owncloud.android.domain.exceptions.FileNotFoundException import com.owncloud.android.domain.exceptions.ForbiddenException +import com.owncloud.android.domain.exceptions.IncompleteFileDataException import com.owncloud.android.domain.exceptions.IncorrectAddressException import com.owncloud.android.domain.exceptions.InstanceNotConfiguredException import com.owncloud.android.domain.exceptions.InvalidOverwriteException @@ -84,6 +85,7 @@ fun Throwable.parseError( }) is FileNotFoundException -> resources.getString(R.string.common_not_found) is ForbiddenException -> resources.getString(R.string.uploads_view_upload_status_failed_permission_error) + is IncompleteFileDataException -> resources.getString(R.string.get_graph_shares_incomplete_data_error) is IncorrectAddressException -> resources.getString(R.string.auth_incorrect_address_title) is InstanceNotConfiguredException -> resources.getString(R.string.auth_not_configured_title) is InvalidOverwriteException -> resources.getString(R.string.file_already_exists) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt index 9777ec03d6a..4869ed3fb9c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt @@ -43,6 +43,11 @@ class ReleaseNotesViewModel( companion object { val releaseNotesList = listOf( + ReleaseNote( + title = R.string.release_notes_4_9_0_sharing_ng_title, + subtitle = R.string.release_notes_4_9_0_sharing_ng_subtitle, + type = ReleaseNoteType.ENHANCEMENT + ), ReleaseNote( title = R.string.release_notes_bugfixes_title, subtitle = R.string.release_notes_bugfixes_subtitle, 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 new file mode 100644 index 00000000000..792f7f50004 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt @@ -0,0 +1,146 @@ +/** + * 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.presentation.sharing + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +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.extensions.collectLatestLifecycleFlow +import com.owncloud.android.extensions.showErrorInSnackbar +import com.owncloud.android.presentation.common.UIResult +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import timber.log.Timber + +class GraphShareFragment : Fragment() { + private var _binding: MembersFragmentBinding? = null + private val binding get() = _binding!! + + private val graphShareViewModel by viewModel { + parametersOf( + requireArguments().getString(ARG_ACCOUNT_NAME), + requireArguments().getParcelable(ARG_FILE) + ) + } + + private lateinit var graphSharesAdapter: GraphSharesAdapter + + private var roles: List = emptyList() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = MembersFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.membersTitle.text = getString(R.string.share_with_people_title) + + graphSharesAdapter = GraphSharesAdapter() + binding.membersRecyclerView.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = graphSharesAdapter + } + + binding.swipeRefreshMembers.setOnRefreshListener { + graphShareViewModel.getGraphShares() + } + + subscribeToViewModels() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun subscribeToViewModels() { + observeRoles() + observeShares() + } + + 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") + } + } + } + } + } + + private fun observeShares() { + collectLatestLifecycleFlow(graphShareViewModel.shares) { 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 + } + } + 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 shares") + } + } + } + } + } + + companion object { + private const val ARG_FILE = "FILE" + private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME" + + fun newInstance(file: OCFile, accountName: String): GraphShareFragment { + val args = Bundle().apply { + putParcelable(ARG_FILE, file) + putString(ARG_ACCOUNT_NAME, accountName) + } + return GraphShareFragment().apply { + arguments = args + } + } + } +} 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 new file mode 100644 index 00000000000..d332f781caf --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt @@ -0,0 +1,81 @@ +/** + * 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.presentation.sharing + +import androidx.lifecycle.ViewModel +import com.owncloud.android.domain.exceptions.IncompleteFileDataException +import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.domain.roles.model.OCRole +import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase +import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUseCase +import com.owncloud.android.domain.sharing.shares.model.OCPermissions +import com.owncloud.android.domain.utils.Event +import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult +import com.owncloud.android.presentation.common.UIResult +import com.owncloud.android.providers.CoroutinesDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class GraphShareViewModel( + private val getRolesAsyncUseCase: GetRolesAsyncUseCase, + private val getGraphSharesAsyncUseCase: GetGraphSharesAsyncUseCase, + 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 + + init { + runUseCaseWithResult( + coroutineDispatcher = coroutineDispatcherProvider.io, + flow = _roles, + useCase = getRolesAsyncUseCase, + useCaseParams = GetRolesAsyncUseCase.Params(accountName = accountName), + ) + } + + fun getGraphShares() { + val spaceId = file.spaceId + val itemId = file.remoteId + if (spaceId == null || itemId == null) { + _shares.update { Event(UIResult.Error(error = IncompleteFileDataException())) } + return + } + + runUseCaseWithResult( + coroutineDispatcher = coroutineDispatcherProvider.io, + showLoading = true, + flow = _shares, + useCase = getGraphSharesAsyncUseCase, + useCaseParams = GetGraphSharesAsyncUseCase.Params( + accountName = accountName, + spaceId = spaceId, + itemId = itemId + ) + ) + } +} 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 new file mode 100644 index 00000000000..bc765fe678c --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesAdapter.kt @@ -0,0 +1,88 @@ +/** + * 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.presentation.sharing + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R +import com.owncloud.android.databinding.MemberItemBinding +import com.owncloud.android.domain.roles.model.OCRole +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() { + + private var shares: List = emptyList() + private var rolesMap: Map = emptyMap() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphShareViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.member_item, parent, false) + view.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(parent.context) + return GraphShareViewHolder(view) + } + + override fun onBindViewHolder(holder: GraphShareViewHolder, position: Int) { + val share = shares[position] + val roleNames = share.roles.mapNotNull { rolesMap[it] } + + holder.binding.apply { + memberIcon.setImageResource(if (share.isGroup) R.drawable.ic_group else R.drawable.ic_user) + memberName.text = share.displayName + memberName.contentDescription = holder.itemView.context.getString( + if (share.isGroup) R.string.content_description_member_group else R.string.content_description_member_user, + share.displayName + ) + memberRole.text = roleNames.joinToString(", ") + + val hasExpirationDate = share.expirationDateTime != null + expirationCalendarIcon.isVisible = hasExpirationDate + expirationDate.isVisible = hasExpirationDate + if (hasExpirationDate) { + expirationDate.text = DisplayUtils.displayDateToHumanReadable(share.expirationDateTime) + expirationDate.contentDescription = + holder.itemView.context.getString(R.string.content_description_member_expiration_date, expirationDate.text) + } + } + } + + override fun getItemCount(): Int = shares.size + + fun setShares(shares: List, roles: List) { + 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)) + this.shares = sortedShares + diffResult.dispatchUpdatesTo(this) + } + + class GraphShareViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val binding = MemberItemBinding.bind(itemView) + } +} 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 new file mode 100644 index 00000000000..39bf62427ee --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphSharesDiffUtil.kt @@ -0,0 +1,40 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.sharing + +import androidx.recyclerview.widget.DiffUtil +import com.owncloud.android.domain.sharing.shares.model.MemberPermission + +class GraphSharesDiffUtil( + private val oldList: List, + private val newList: List, +) : DiffUtil.Callback() { + + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition].id == newList[newItemPosition].id + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] +} 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 8aba893d508..72b8c5faec7 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 @@ -93,6 +93,11 @@ class ShareActivity : FileActivity(), ShareFragmentListener { setContentView(binding.root) setupToolbar(binding.root) setupFileHeader(binding) + if (file != null && account != null) { + supportFragmentManager.transaction { + replace(R.id.members_fragment_container, GraphShareFragment.newInstance(file, account!!.name), TAG_GRAPH_SHARE_FRAGMENT) + } + } } private fun setupLegacyLayout(savedInstanceState: Bundle?) { @@ -355,7 +360,8 @@ class ShareActivity : FileActivity(), ShareFragmentListener { override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean = if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - if (findViewById(R.id.owncloud_app_bar).hasFocus()) { + val appBar = findViewById(R.id.owncloud_app_bar) + if (appBar?.hasFocus() == true) { findViewById(R.id.share_fragment_container).requestFocus() } true @@ -365,6 +371,7 @@ class ShareActivity : FileActivity(), ShareFragmentListener { companion object { const val TAG_SHARE_FRAGMENT = "SHARE_FRAGMENT" + const val TAG_GRAPH_SHARE_FRAGMENT = "GRAPH_SHARE_FRAGMENT" const val TAG_SEARCH_FRAGMENT = "SEARCH_USER_AND_GROUPS_FRAGMENT" const val TAG_EDIT_SHARE_FRAGMENT = "EDIT_SHARE_FRAGMENT" const val TAG_PUBLIC_SHARE_DIALOG_FRAGMENT = "PUBLIC_SHARE_DIALOG_FRAGMENT" diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt index 755b9c402c9..c4d6e530c55 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt @@ -36,7 +36,7 @@ import com.owncloud.android.domain.members.model.OCMember import com.owncloud.android.domain.members.model.OCMemberType import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.domain.spaces.model.SpaceMember +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.presentation.common.UIResult @@ -91,7 +91,7 @@ class AddMemberFragment: Fragment(), SearchMembersAdapter.SearchMembersAdapterLi roles = requireArguments().getParcelableArrayList(ARG_ROLES) ?: arrayListOf() if (editMode) { - val selectedMember = requireArguments().getParcelable(ARG_SELECTED_MEMBER) + val selectedMember = requireArguments().getParcelable(ARG_SELECTED_MEMBER) selectedMember?.let { bindEditMode(it, roles) } @@ -141,7 +141,7 @@ class AddMemberFragment: Fragment(), SearchMembersAdapter.SearchMembersAdapterLi } private fun subscribeToViewModels() { - val spaceMembers = requireArguments().getParcelableArrayList(ARG_SPACE_MEMBERS) ?: arrayListOf() + val spaceMembers = requireArguments().getParcelableArrayList(ARG_SPACE_MEMBERS) ?: arrayListOf() searchMinLength = spaceMembersViewModel.capabilities?.filesSharingSearchMinLength ?: DEFAULT_SEARCH_MIN_LENGTH collectLatestLifecycleFlow(spaceMembersViewModel.members) { uiState -> @@ -299,7 +299,7 @@ class AddMemberFragment: Fragment(), SearchMembersAdapter.SearchMembersAdapterLi } } - private fun bindEditMode(member: SpaceMember, roles: List) { + private fun bindEditMode(member: MemberPermission, roles: List) { selectedMemberId = member.id spaceMembersViewModel.onMemberSelected(member) @@ -325,10 +325,10 @@ class AddMemberFragment: Fragment(), SearchMembersAdapter.SearchMembersAdapterLi fun newInstance( accountName: String, currentSpace: OCSpace, - spaceMembers: List, + spaceMembers: List, roles: List, editMode: Boolean, - selectedMember: SpaceMember? + selectedMember: MemberPermission? ): AddMemberFragment { val args = Bundle().apply { putString(ARG_ACCOUNT_NAME, accountName) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt index cffeda728d2..4f447463562 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt @@ -31,7 +31,7 @@ import com.owncloud.android.databinding.MembersActivityBinding import com.owncloud.android.domain.links.model.OCLink import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.domain.spaces.model.SpaceMember +import com.owncloud.android.domain.sharing.shares.model.MemberPermission import com.owncloud.android.extensions.adaptInfiniteEdges import com.owncloud.android.presentation.common.ShareSheetHelper import com.owncloud.android.presentation.spaces.links.AddPublicLinkFragment @@ -95,7 +95,13 @@ class SpaceMembersActivity: FileActivity(), SpaceMembersFragment.SpaceMemberFrag super.onOptionsItemSelected(item) } - override fun addMember(space: OCSpace, spaceMembers: List, roles: List, editMode: Boolean, selectedMember: SpaceMember?) { + override fun addMember( + space: OCSpace, + spaceMembers: List, + roles: List, + editMode: Boolean, + selectedMember: MemberPermission? + ) { val addMemberFragment = AddMemberFragment.newInstance(account.name, space, spaceMembers, roles, editMode, selectedMember) val transaction = supportFragmentManager.beginTransaction() transaction.apply { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersAdapter.kt index b7190b5c33d..021b3b73bf0 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersAdapter.kt @@ -30,7 +30,7 @@ import com.owncloud.android.R import com.owncloud.android.databinding.MemberItemBinding import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.roles.model.OCRoleType -import com.owncloud.android.domain.spaces.model.SpaceMember +import com.owncloud.android.domain.sharing.shares.model.MemberPermission import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.PreferenceUtils @@ -39,7 +39,7 @@ class SpaceMembersAdapter( private val accountId: String? ): RecyclerView.Adapter() { - private var members: List = emptyList() + private var members: List = emptyList() private var rolesMap: Map = emptyMap() private var canRemoveMembers = false private var canEditMembers = false @@ -105,7 +105,7 @@ class SpaceMembersAdapter( override fun getItemCount(): Int = members.size fun setSpaceMembers( - spaceMembers: List, + spaceMembers: List, roles: List, canRemoveMembers: Boolean, canEditMembers: Boolean, @@ -118,7 +118,7 @@ class SpaceMembersAdapter( this.canRemoveMembers = canRemoveMembers this.canEditMembers = canEditMembers this.rolesMap = roles.associate { it.id to it.displayName } - val listOfMembersFiltered = spaceMembers.sortedWith(compareByDescending { + val listOfMembersFiltered = spaceMembers.sortedWith(compareByDescending { member -> roles.indexOfFirst { it.id in member.roles } }.thenBy { member -> member.displayName }) val diffCallback = SpaceMembersDiffUtil(this.members, listOfMembersFiltered, numberOfManagersChanged, hasUserPermissionsChanged) val diffResult = DiffUtil.calculateDiff(diffCallback) @@ -132,8 +132,8 @@ class SpaceMembersAdapter( } interface SpaceMembersAdapterListener { - fun onRemoveMember(spaceMember: SpaceMember) - fun onEditMember(spaceMember: SpaceMember) + fun onRemoveMember(spaceMember: MemberPermission) + fun onEditMember(spaceMember: MemberPermission) } companion object { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersDiffUtil.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersDiffUtil.kt index 637319c56a1..581c8864811 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersDiffUtil.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersDiffUtil.kt @@ -21,11 +21,11 @@ package com.owncloud.android.presentation.spaces.members import androidx.recyclerview.widget.DiffUtil -import com.owncloud.android.domain.spaces.model.SpaceMember +import com.owncloud.android.domain.sharing.shares.model.MemberPermission class SpaceMembersDiffUtil( - private val oldList: List, - private val newList: List, + private val oldList: List, + private val newList: List, private val hasNumberOfManagersChanged: Boolean, private val hasUserPermissionsChanged: Boolean ) : DiffUtil.Callback() { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt index 66da33336ca..97ce41306cb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt @@ -36,7 +36,7 @@ import com.owncloud.android.domain.links.model.OCLink import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.roles.model.OCRoleType import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.domain.spaces.model.SpaceMember +import com.owncloud.android.domain.sharing.shares.model.MemberPermission import com.owncloud.android.extensions.avoidScreenshotsIfNeeded import com.owncloud.android.extensions.collectLatestLifecycleFlow import com.owncloud.android.extensions.showAlertDialog @@ -77,7 +77,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter private var roles: List = emptyList() private var addMemberRoles: List = emptyList() - private var spaceMembers: List = emptyList() + private var spaceMembers: List = emptyList() private var spaceLinks: List = emptyList() private var listener: SpaceMemberFragmentListener? = null private var canRemoveMembersAndLinks = false @@ -172,7 +172,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter outState.putBoolean(CAN_READ_MEMBERS, canReadMembersAndLinks) } - override fun onRemoveMember(spaceMember: SpaceMember) { + override fun onRemoveMember(spaceMember: MemberPermission) { AlertDialog.Builder(requireContext()) .setMessage(getString(R.string.members_remove_dialog_message, spaceMember.displayName)) .setPositiveButton(getString(R.string.common_yes)) { _, _ -> spaceMembersViewModel.removeMember(spaceMember.id) } @@ -181,7 +181,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter .avoidScreenshotsIfNeeded() } - override fun onEditMember(spaceMember: SpaceMember) { + override fun onEditMember(spaceMember: MemberPermission) { spaceMembersViewModel.resetViewModel() val currentSpace = requireArguments().getParcelable(ARG_CURRENT_SPACE) ?: return listener?.addMember( @@ -458,7 +458,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } interface SpaceMemberFragmentListener { - fun addMember(space: OCSpace, spaceMembers: List, roles: List, editMode: Boolean, selectedMember: SpaceMember?) + fun addMember(space: OCSpace, spaceMembers: List, roles: List, editMode: Boolean, selectedMember: MemberPermission?) fun addPublicLink(space: OCSpace, editMode: Boolean, selectedPublicLink: OCLink?) fun copyOrSendPublicLink(publicLinkUrl: String, spaceName: String) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt index 87c3129b238..978dcf0bb72 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt @@ -31,12 +31,12 @@ import com.owncloud.android.domain.members.usecases.EditMemberUseCase import com.owncloud.android.domain.members.usecases.RemoveMemberUseCase import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.domain.spaces.model.SpaceMembers +import com.owncloud.android.domain.sharing.shares.model.OCPermissions import com.owncloud.android.domain.spaces.usecases.GetSpaceMembersUseCase import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase import com.owncloud.android.domain.members.usecases.SearchMembersUseCase -import com.owncloud.android.domain.spaces.model.SpaceMember +import com.owncloud.android.domain.sharing.shares.model.MemberPermission import com.owncloud.android.domain.utils.Event import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.extensions.toOCMember @@ -67,8 +67,8 @@ class SpaceMembersViewModel( private val _roles = MutableStateFlow>>?>(null) val roles: StateFlow>>?> = _roles - private val _spaceMembers = MutableStateFlow>?>(null) - val spaceMembers: StateFlow>?> = _spaceMembers + private val _spaceMembers = MutableStateFlow>?>(null) + val spaceMembers: StateFlow>?> = _spaceMembers private val _spacePermissions = MutableStateFlow>>?>(null) val spacePermissions: StateFlow>>?> = _spacePermissions @@ -146,7 +146,7 @@ class SpaceMembersViewModel( _addMemberUIState.value = AddMemberUIState(selectedMember = member) } - fun onMemberSelected(member: SpaceMember) { + fun onMemberSelected(member: MemberPermission) { onMemberSelected(member.toOCMember()) } diff --git a/owncloudApp/src/main/res/layout/members_fragment.xml b/owncloudApp/src/main/res/layout/members_fragment.xml index 858cfe858ac..ea134cf8381 100644 --- a/owncloudApp/src/main/res/layout/members_fragment.xml +++ b/owncloudApp/src/main/res/layout/members_fragment.xml @@ -83,6 +83,18 @@ android:layout_height="wrap_content" android:scrollbars="vertical"/> + + %1$s could not be copied to %2$s local folder. It was not possible to retrieve the shares from the server + Some data could not be retrieved It was not possible to retrieve users and groups from the server It was not possible to retrieve the capabilities from the server It was not possible to share this file or folder @@ -492,6 +493,7 @@ Share Users and Groups No data shared with users yet + Shares could not be refreshed Public links Create link share Edit link share @@ -506,6 +508,7 @@ Download/View/Upload Upload Only (File Drop) Share with + Share with people Share with %1$s Search @@ -623,6 +626,8 @@ Release note icon Minor bugfixes Some bugs were fixed to improve experience in the app + New sharing implementation for Infinite Scale + New interface and operations have been added to share files and folders on Infinite Scale accounts Open in %1$s (web) diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/GetRemoteGraphSharesForFileOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/GetRemoteGraphSharesForFileOperation.kt new file mode 100644 index 00000000000..bda20f0ca78 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/GetRemoteGraphSharesForFileOperation.kt @@ -0,0 +1,82 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.lib.resources.shares + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod +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 com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import timber.log.Timber +import java.net.URL + +class GetRemoteGraphSharesForFileOperation( + private val spaceId: String, + private val itemId: String, +) : RemoteOperation() { + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult + try { + val requestUri = client.baseUri.buildUpon().apply { + appendEncodedPath(GRAPH_API_DRIVES_PATH) + appendEncodedPath(spaceId) + appendEncodedPath(ITEMS_PATH) + appendEncodedPath(itemId) + appendEncodedPath(PERMISSIONS_ENDPOINT) + build() + } + val getMethod = GetMethod(URL(requestUri.toString())) + + val status = client.executeHttpMethod(getMethod) + + val response = getMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_OK) { + Timber.d("Successful response: $response") + + val moshi: Moshi = Moshi.Builder().build() + val adapter: JsonAdapter = moshi.adapter(PermissionsResponse::class.java) + + result = RemoteOperationResult(ResultCode.OK) + result.data = getMethod.getResponseBodyAsString().let { adapter.fromJson(it) } + + Timber.d("Get graph shares completed and parsed to ${result.data}") + } else { + result = RemoteOperationResult(getMethod) + Timber.e("Failed response while getting graph shares; status code: $status, response: $response") + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Timber.e(e, "Exception while getting graph shares") + } + return result + } + + companion object { + private const val GRAPH_API_DRIVES_PATH = "graph/v1beta1/drives/" + private const val ITEMS_PATH = "items" + private const val PERMISSIONS_ENDPOINT = "permissions" + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/ShareService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/shares/services/ShareService.kt index 53458c59186..b81cb8fcab5 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 @@ -30,6 +30,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.Service import com.owncloud.android.lib.resources.shares.ShareResponse import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse interface ShareService : Service { fun getShares( @@ -38,6 +39,8 @@ interface ShareService : Service { subfiles: Boolean ): RemoteOperationResult + fun getGraphShares(spaceId: String, itemId: 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 af2845293b4..fa9fc9ebf71 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 @@ -29,12 +29,14 @@ package com.owncloud.android.lib.resources.shares.services.implementation import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.shares.CreateRemoteShareOperation +import com.owncloud.android.lib.resources.shares.GetRemoteGraphSharesForFileOperation import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation import com.owncloud.android.lib.resources.shares.RemoveRemoteShareOperation import com.owncloud.android.lib.resources.shares.ShareResponse import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.shares.UpdateRemoteShareOperation import com.owncloud.android.lib.resources.shares.services.ShareService +import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse class OCShareService(override val client: OwnCloudClient) : ShareService { override fun getShares( @@ -47,6 +49,9 @@ class OCShareService(override val client: OwnCloudClient) : ShareService { subfiles ).execute(client) + override fun getGraphShares(spaceId: String, itemId: String): RemoteOperationResult = + GetRemoteGraphSharesForFileOperation(spaceId, itemId).execute(client) + override fun insertShare( remoteFilePath: String, shareType: ShareType, diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt index fa41aea6d32..dbdaf5cbabb 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt @@ -26,7 +26,7 @@ import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod 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 com.owncloud.android.lib.resources.spaces.responses.SpacePermissionsResponse +import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import timber.log.Timber @@ -34,9 +34,9 @@ import java.net.URL class GetRemoteSpacePermissionsOperation( private val spaceId: String -): RemoteOperation() { - override fun run(client: OwnCloudClient): RemoteOperationResult { - var result: RemoteOperationResult +): RemoteOperation() { + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult try { val requestUri = client.baseUri.buildUpon().apply { appendEncodedPath(GRAPH_API_SPACES_PATH) @@ -54,7 +54,7 @@ class GetRemoteSpacePermissionsOperation( Timber.d("Successful response: $response") val moshi: Moshi = Moshi.Builder().build() - val adapter: JsonAdapter = moshi.adapter(SpacePermissionsResponse::class.java) + val adapter: JsonAdapter = moshi.adapter(PermissionsResponse::class.java) result = RemoteOperationResult(ResultCode.OK) result.data = getMethod.getResponseBodyAsString().let { adapter.fromJson(it) } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/responses/SpacesResponse.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/responses/SpacesResponse.kt index 4f351f18a0a..d03b80af775 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/responses/SpacesResponse.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/responses/SpacesResponse.kt @@ -66,7 +66,7 @@ data class RootResponse( val id: String, val webDavUrl: String, val deleted: DeleteResponse?, - val permissions: List? + val permissions: List? ) @JsonClass(generateAdapter = true) @@ -103,7 +103,7 @@ data class SpecialFolderResponse( ) @JsonClass(generateAdapter = true) -data class PermissionsResponse( +data class MemberPermissionResponse( val id: String?, val expirationDateTime: String?, @@ -130,13 +130,13 @@ data class GroupResponse( ) @JsonClass(generateAdapter = true) -data class SpacePermissionsResponse( +data class PermissionsResponse( @Json(name = "@libre.graph.permissions.actions.allowedValues") val actions: List, @Json(name = "@libre.graph.permissions.roles.allowedValues") val roles: List, @Json(name = "value") - val members: List + val members: List? ) @JsonClass(generateAdapter = true) diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt index ea71a2173d9..af65670bac7 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt @@ -31,7 +31,7 @@ import com.owncloud.android.lib.resources.spaces.EditRemoteSpaceOperation import com.owncloud.android.lib.resources.spaces.EnableRemoteSpaceOperation import com.owncloud.android.lib.resources.spaces.GetRemoteSpacePermissionsOperation import com.owncloud.android.lib.resources.spaces.GetRemoteSpacesOperation -import com.owncloud.android.lib.resources.spaces.responses.SpacePermissionsResponse +import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse class OCSpacesService(override val client: OwnCloudClient) : SpacesService { @@ -41,7 +41,7 @@ class OCSpacesService(override val client: OwnCloudClient) : SpacesService { override fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long): RemoteOperationResult = CreateRemoteSpaceOperation(spaceName, spaceSubtitle, spaceQuota).execute(client) - override fun getSpacePermissions(spaceId: String): RemoteOperationResult = + override fun getSpacePermissions(spaceId: String): RemoteOperationResult = GetRemoteSpacePermissionsOperation(spaceId).execute(client) override fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult = diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/SpacesService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/SpacesService.kt index a8eb59e798f..2ecb765c67e 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/SpacesService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/SpacesService.kt @@ -24,13 +24,13 @@ package com.owncloud.android.lib.resources.spaces.services import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.Service -import com.owncloud.android.lib.resources.spaces.responses.SpacePermissionsResponse +import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse interface SpacesService : Service { fun getSpaces(): RemoteOperationResult> fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long): RemoteOperationResult - fun getSpacePermissions(spaceId: String): RemoteOperationResult + fun getSpacePermissions(spaceId: String): RemoteOperationResult fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult fun editSpaceImage(spaceId: String, imageId: String): RemoteOperationResult fun disableSpace(spaceId: String, deleteMode: Boolean): RemoteOperationResult 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 58834dcd13a..e193d8c3f92 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 @@ -2,7 +2,9 @@ * ownCloud Android client application * * @author David González Verdugo - * Copyright (C) 2020 ownCloud GmbH. + * @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, @@ -21,6 +23,7 @@ package com.owncloud.android.data.sharing.shares.datasources import com.owncloud.android.domain.sharing.shares.model.OCShare import com.owncloud.android.domain.sharing.shares.model.ShareType +import com.owncloud.android.domain.sharing.shares.model.OCPermissions import com.owncloud.android.lib.resources.shares.RemoteShare.Companion.INIT_EXPIRATION_DATE_IN_MILLIS interface RemoteShareDataSource { @@ -31,6 +34,12 @@ interface RemoteShareDataSource { accountName: String ): List + fun getGraphShares( + accountName: String, + spaceId: String, + itemId: String + ): OCPermissions + 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 175cc46491f..427ba766347 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 @@ -2,7 +2,9 @@ * ownCloud Android client application * * @author David González Verdugo - * Copyright (C) 2020 ownCloud GmbH. + * @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, @@ -23,8 +25,14 @@ import com.owncloud.android.data.ClientManager import com.owncloud.android.data.executeRemoteOperation import com.owncloud.android.data.sharing.shares.datasources.RemoteShareDataSource import com.owncloud.android.data.sharing.shares.datasources.mapper.RemoteShareMapper +import com.owncloud.android.domain.links.model.OCLink +import com.owncloud.android.domain.links.model.OCLinkType +import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.sharing.shares.model.OCShare import com.owncloud.android.domain.sharing.shares.model.ShareType +import com.owncloud.android.domain.sharing.shares.model.MemberPermission +import com.owncloud.android.domain.sharing.shares.model.OCPermissions +import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse class OCRemoteShareDataSource( private val clientManager: ClientManager, @@ -48,6 +56,17 @@ class OCRemoteShareDataSource( } } + override fun getGraphShares( + accountName: String, + spaceId: String, + itemId: String + ): OCPermissions { + val response = executeRemoteOperation { + clientManager.getShareService(accountName).getGraphShares(spaceId, itemId) + } + return response.toModel() + } + override fun insert( remoteFilePath: String, shareType: ShareType, @@ -103,4 +122,40 @@ class OCRemoteShareDataSource( clientManager.getShareService(accountName).deleteShare(remoteId) } } + + companion object { + fun PermissionsResponse.toModel(): OCPermissions { + val membersResponse = members.orEmpty() + return OCPermissions( + roles = roles.map { spaceRoleResponse -> + OCRole( + id = spaceRoleResponse.id, + displayName = spaceRoleResponse.displayName, + description = spaceRoleResponse.description + ) + }, + members = membersResponse.filter { it.grantedToV2 != null }.map { spaceMemberResponse -> + MemberPermission( + id = spaceMemberResponse.id ?: "", + expirationDateTime = spaceMemberResponse.expirationDateTime, + displayName = spaceMemberResponse.grantedToV2?.user?.displayName + ?: spaceMemberResponse.grantedToV2?.group?.displayName ?: "", + roles = spaceMemberResponse.roles ?: emptyList(), + isGroup = spaceMemberResponse.grantedToV2?.group != null + ) + }, + links = membersResponse.filter { it.grantedToV2 == null }.map { spaceLinkResponse -> + OCLink( + id = spaceLinkResponse.id.orEmpty(), + createdDateTime = spaceLinkResponse.createdDateTime.orEmpty(), + expirationDateTime = spaceLinkResponse.expirationDateTime, + hasPassword = spaceLinkResponse.hasPassword ?: false, + displayName = spaceLinkResponse.link?.displayName.orEmpty(), + type = OCLinkType.parseFromString(spaceLinkResponse.link?.type.orEmpty()), + webUrl = spaceLinkResponse.link?.webUrl.orEmpty() + ) + } + ) + } + } } 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 1bcf69c74f6..0f33d4f3dab 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 @@ -3,8 +3,9 @@ * * @author David González Verdugo * @author Juan Carlos Garrote Gascón + * @author Jorge Aguado Recio * - * Copyright (C) 2022 ownCloud GmbH. + * 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, @@ -27,6 +28,7 @@ import com.owncloud.android.data.sharing.shares.datasources.RemoteShareDataSourc import com.owncloud.android.domain.sharing.shares.ShareRepository import com.owncloud.android.domain.sharing.shares.model.OCShare import com.owncloud.android.domain.sharing.shares.model.ShareType +import com.owncloud.android.domain.sharing.shares.model.OCPermissions import com.owncloud.android.lib.resources.shares.RemoteShare class OCShareRepository( @@ -118,6 +120,9 @@ class OCShareRepository( override fun getShareAsLiveData(remoteId: String): LiveData = localShareDataSource.getShareAsLiveData(remoteId) + override fun getGraphShares(accountName: String, spaceId: String, itemId: String): OCPermissions = + remoteShareDataSource.getGraphShares(accountName, spaceId, itemId) + override fun refreshSharesFromNetwork( filePath: String, accountName: String diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt index 776e7ca5d1d..c89dfb44863 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt @@ -21,12 +21,12 @@ package com.owncloud.android.data.spaces.datasources import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.domain.spaces.model.SpaceMembers +import com.owncloud.android.domain.sharing.shares.model.OCPermissions interface RemoteSpacesDataSource { fun refreshSpacesForAccount(accountName: String, userId: String, userGroups: List): List fun createSpace(accountName: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long): OCSpace - fun getSpaceMembers(accountName: String, spaceId: String): SpaceMembers + fun getSpaceMembers(accountName: String, spaceId: String): OCPermissions fun getSpacePermissions(accountName: String, spaceId: String): List fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): OCSpace fun editSpaceImage(accountName: String, spaceId: String, imageId: String): OCSpace diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt index 6029b2cc9fb..c5751a11c0e 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt @@ -30,8 +30,8 @@ import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.domain.spaces.model.SpaceDeleted import com.owncloud.android.domain.spaces.model.SpaceFile -import com.owncloud.android.domain.spaces.model.SpaceMember -import com.owncloud.android.domain.spaces.model.SpaceMembers +import com.owncloud.android.domain.sharing.shares.model.MemberPermission +import com.owncloud.android.domain.sharing.shares.model.OCPermissions import com.owncloud.android.domain.spaces.model.SpaceOwner import com.owncloud.android.domain.spaces.model.SpaceQuota import com.owncloud.android.domain.spaces.model.SpaceRoot @@ -39,7 +39,7 @@ import com.owncloud.android.domain.spaces.model.SpaceSpecial import com.owncloud.android.domain.spaces.model.SpaceSpecialFolder import com.owncloud.android.domain.spaces.model.SpaceUser import com.owncloud.android.lib.resources.spaces.responses.RootResponse -import com.owncloud.android.lib.resources.spaces.responses.SpacePermissionsResponse +import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse class OCRemoteSpacesDataSource( @@ -60,7 +60,7 @@ class OCRemoteSpacesDataSource( return spaceResponse.toModel(accountName) } - override fun getSpaceMembers(accountName: String, spaceId: String): SpaceMembers { + override fun getSpaceMembers(accountName: String, spaceId: String): OCPermissions { val spacePermissionsResponse = executeRemoteOperation { clientManager.getSpacesService(accountName).getSpacePermissions(spaceId) } @@ -196,8 +196,9 @@ class OCRemoteSpacesDataSource( ) @VisibleForTesting - fun SpacePermissionsResponse.toModel(): SpaceMembers = - SpaceMembers( + fun PermissionsResponse.toModel(): OCPermissions { + val membersResponse = members.orEmpty() + return OCPermissions( roles = roles.map { spaceRoleResponse -> OCRole( id = spaceRoleResponse.id, @@ -205,16 +206,17 @@ class OCRemoteSpacesDataSource( description = spaceRoleResponse.description ) }, - members = members.filter { it.grantedToV2 != null }.map { spaceMemberResponse -> - SpaceMember ( + members = membersResponse.filter { it.grantedToV2 != null }.map { spaceMemberResponse -> + MemberPermission( id = spaceMemberResponse.id ?: "", expirationDateTime = spaceMemberResponse.expirationDateTime, displayName = spaceMemberResponse.grantedToV2?.user?.displayName ?: spaceMemberResponse.grantedToV2?.group?.displayName ?: "", - roles = spaceMemberResponse.roles ?: emptyList() + roles = spaceMemberResponse.roles ?: emptyList(), + isGroup = spaceMemberResponse.grantedToV2?.group != null ) }, - links = members.filter { it.grantedToV2 == null }.map { spaceLinkResponse -> + links = membersResponse.filter { it.grantedToV2 == null }.map { spaceLinkResponse -> OCLink( id = spaceLinkResponse.id.orEmpty(), createdDateTime = spaceLinkResponse.createdDateTime.orEmpty(), @@ -226,6 +228,7 @@ class OCRemoteSpacesDataSource( ) } ) + } private fun getRoleForUser(root: RootResponse, userId: String, userGroups: List): String? { diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt index e7553caed01..a41ec4ca0e0 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt @@ -28,7 +28,7 @@ import com.owncloud.android.data.spaces.datasources.RemoteSpacesDataSource import com.owncloud.android.data.user.datasources.LocalUserDataSource import com.owncloud.android.domain.spaces.SpacesRepository import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.domain.spaces.model.SpaceMembers +import com.owncloud.android.domain.sharing.shares.model.OCPermissions import com.owncloud.android.domain.user.model.UserQuotaState import com.owncloud.android.domain.user.model.UserQuota @@ -75,7 +75,7 @@ class OCSpacesRepository( override fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace? = localSpacesDataSource.getSpaceByIdForAccount(spaceId = spaceId, accountName = accountName) - override fun getSpaceMembers(accountName: String, spaceId: String): SpaceMembers = + override fun getSpaceMembers(accountName: String, spaceId: String): OCPermissions = remoteSpacesDataSource.getSpaceMembers(accountName, spaceId) override fun getSpacePermissions(accountName: String, spaceId: String): List = 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 44334c0b58f..52ee3ab4747 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 @@ -4,8 +4,9 @@ * @author David González Verdugo * @author Jesús Recio * @author Aitor Ballesteros Pavón + * @author Jorge Aguado Recio * - * Copyright (C) 2023 ownCloud GmbH. + * 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, @@ -23,6 +24,7 @@ package com.owncloud.android.data.sharing.shares.datasources.implementation import com.owncloud.android.data.ClientManager +import com.owncloud.android.data.sharing.shares.datasources.implementation.OCRemoteShareDataSource.Companion.toModel import com.owncloud.android.data.sharing.shares.datasources.mapper.RemoteShareMapper import com.owncloud.android.domain.exceptions.ShareForbiddenException import com.owncloud.android.domain.exceptions.ShareNotFoundException @@ -31,7 +33,10 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.shares.ShareResponse import com.owncloud.android.lib.resources.shares.services.implementation.OCShareService import com.owncloud.android.testutil.OC_ACCOUNT_NAME +import com.owncloud.android.testutil.OC_FILE import com.owncloud.android.testutil.OC_SHARE +import com.owncloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE +import com.owncloud.android.testutil.SPACE_PERMISSIONS_RESPONSE import com.owncloud.android.utils.createRemoteOperationResultMock import io.mockk.every import io.mockk.mockk @@ -386,6 +391,23 @@ class OCRemoteShareDataSourceTest { } } + @Test + fun `getGraphShares returns a OCPermissions`() { + val getSpaceMembersResult = createRemoteOperationResultMock(SPACE_PERMISSIONS_RESPONSE, isSuccess = true) + + every { + ocShareService.getGraphShares(OC_SPACE_PROJECT_WITH_IMAGE.id, OC_FILE.remoteId.orEmpty()) + } returns getSpaceMembersResult + + val ocisShares = ocRemoteShareDataSource.getGraphShares(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id, OC_FILE.remoteId.orEmpty()) + assertEquals(SPACE_PERMISSIONS_RESPONSE.toModel(), ocisShares) + + verify(exactly = 1) { + clientManager.getShareService(OC_ACCOUNT_NAME) + ocShareService.getGraphShares(OC_SPACE_PROJECT_WITH_IMAGE.id, OC_FILE.remoteId.orEmpty()) + } + } + @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 50fdb95c897..d5c999a9e65 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 @@ -2,7 +2,9 @@ * ownCloud Android client application * * @author David González Verdugo - * Copyright (C) 2020 ownCloud GmbH. + * @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, @@ -28,10 +30,13 @@ import com.owncloud.android.domain.sharing.shares.model.OCShare import com.owncloud.android.domain.sharing.shares.model.ShareType import com.owncloud.android.lib.resources.shares.RemoteShare import com.owncloud.android.testutil.OC_ACCOUNT_NAME +import com.owncloud.android.testutil.OC_FILE import com.owncloud.android.testutil.OC_PRIVATE_SHARE import com.owncloud.android.testutil.OC_PUBLIC_SHARE import com.owncloud.android.testutil.OC_SHARE import com.owncloud.android.testutil.OC_SHAREE +import com.owncloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE +import com.owncloud.android.testutil.SPACE_MEMBERS import com.owncloud.android.testutil.livedata.getLastEmittedValue import io.mockk.every import io.mockk.mockk @@ -237,6 +242,20 @@ class OCShareRepositoryTest { } } + @Test + fun `getGraphShares returns a OCPermissions`() { + every { + remoteShareDataSource.getGraphShares(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id, OC_FILE.remoteId.orEmpty()) + } returns SPACE_MEMBERS + + val sharesResult = ocShareRepository.getGraphShares(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id, OC_FILE.remoteId.orEmpty()) + assertEquals(SPACE_MEMBERS, sharesResult) + + verify(exactly = 1) { + remoteShareDataSource.getGraphShares(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id, OC_FILE.remoteId.orEmpty()) + } + } + @Test fun `refreshSharesFromNetwork refreshes shares correctly when the list of shares received is not empty`() { every { diff --git a/owncloudData/src/test/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSourceTest.kt index 98acddf19b6..7b39bf1e0a8 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSourceTest.kt @@ -102,7 +102,7 @@ class OCRemoteSpacesDataSourceTest { } @Test - fun `getSpaceMembers returns a SpaceMembers for a project space`() { + fun `getSpaceMembers returns a OCPermissions for a project space`() { val getSpaceMembersResult = createRemoteOperationResultMock(SPACE_PERMISSIONS_RESPONSE, isSuccess = true) every { diff --git a/owncloudData/src/test/java/com/owncloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt index 6be5378fa2b..df1a96d7565 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt @@ -252,7 +252,7 @@ class OCSpacesRepositoryTest { } @Test - fun `getSpaceMembers returns a SpaceMembers`() { + fun `getSpaceMembers returns a OCPermissions`() { every { remoteSpacesDataSource.getSpaceMembers(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) } returns SPACE_MEMBERS diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/IncompleteFileDataException.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/IncompleteFileDataException.kt new file mode 100644 index 00000000000..4fccfae1d5c --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/IncompleteFileDataException.kt @@ -0,0 +1,23 @@ +/** + * 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.exceptions + +class IncompleteFileDataException : Exception() 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 cbe4be07a07..4f8f4acfae5 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 @@ -3,8 +3,9 @@ * * @author David González Verdugo * @author Juan Carlos Garrote Gascón + * @author Jorge Aguado Recio * - * Copyright (C) 2022 ownCloud GmbH. + * 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, @@ -24,6 +25,7 @@ package com.owncloud.android.domain.sharing.shares import androidx.lifecycle.LiveData import com.owncloud.android.domain.sharing.shares.model.OCShare import com.owncloud.android.domain.sharing.shares.model.ShareType +import com.owncloud.android.domain.sharing.shares.model.OCPermissions interface ShareRepository { @@ -75,6 +77,8 @@ interface ShareRepository { fun getShareAsLiveData(remoteId: String): LiveData + fun getGraphShares(accountName: String, spaceId: String, itemId: String): OCPermissions + 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/model/OCPermissions.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/model/OCPermissions.kt new file mode 100644 index 00000000000..55f43a5708b --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/model/OCPermissions.kt @@ -0,0 +1,42 @@ +/** + * 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.model + +import android.os.Parcelable +import com.owncloud.android.domain.links.model.OCLink +import com.owncloud.android.domain.roles.model.OCRole +import kotlinx.parcelize.Parcelize + +@Parcelize +data class OCPermissions( + val roles: List, + val members: List, + val links: List +) : Parcelable + +@Parcelize +data class MemberPermission( + val id: String, + val expirationDateTime: String?, + val displayName: String, + val roles: List, + val isGroup: Boolean = false +) : Parcelable diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/GetGraphSharesAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/GetGraphSharesAsyncUseCase.kt new file mode 100644 index 00000000000..7e41b64f7ce --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/GetGraphSharesAsyncUseCase.kt @@ -0,0 +1,35 @@ +/** + * 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 +import com.owncloud.android.domain.sharing.shares.model.OCPermissions + +class GetGraphSharesAsyncUseCase( + private val shareRepository: ShareRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Params) = + shareRepository.getGraphShares(params.accountName, params.spaceId, params.itemId) + + data class Params(val accountName: String, val spaceId: String, val itemId: String) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt index c3a4855ad40..a0bdffe899c 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt @@ -23,7 +23,7 @@ package com.owncloud.android.domain.spaces import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.domain.spaces.model.SpaceMembers +import com.owncloud.android.domain.sharing.shares.model.OCPermissions import kotlinx.coroutines.flow.Flow interface SpacesRepository { @@ -34,7 +34,7 @@ interface SpacesRepository { fun getPersonalAndProjectSpacesForAccount(accountName: String): List fun getSpaceWithSpecialsByIdForAccount(spaceId: String?, accountName: String): OCSpace fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace? - fun getSpaceMembers(accountName: String, spaceId: String): SpaceMembers + fun getSpaceMembers(accountName: String, spaceId: String): OCPermissions fun getSpacePermissions(accountName: String, spaceId: String): List fun getWebDavUrlForSpace(accountName: String, spaceId: String?): String? fun createSpace(accountName: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long) diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt index 1b0811718fc..2220c7bae79 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt @@ -23,8 +23,6 @@ package com.owncloud.android.domain.spaces.model import android.os.Parcelable -import com.owncloud.android.domain.links.model.OCLink -import com.owncloud.android.domain.roles.model.OCRole import kotlinx.parcelize.Parcelize import kotlin.math.roundToLong @@ -123,18 +121,3 @@ data class SpaceFile( data class SpaceSpecialFolder( val name: String ) : Parcelable - -@Parcelize -data class SpaceMembers( - val roles: List, - val members: List, - val links: List -) : Parcelable - -@Parcelize -data class SpaceMember( - val id: String, - val expirationDateTime: String?, - val displayName: String, - val roles: List -) : Parcelable diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpaceMembersUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpaceMembersUseCase.kt index ed40fed5f0d..d175c1b4d2c 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpaceMembersUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpaceMembersUseCase.kt @@ -22,11 +22,11 @@ package com.owncloud.android.domain.spaces.usecases import com.owncloud.android.domain.BaseUseCaseWithResult import com.owncloud.android.domain.spaces.SpacesRepository -import com.owncloud.android.domain.spaces.model.SpaceMembers +import com.owncloud.android.domain.sharing.shares.model.OCPermissions class GetSpaceMembersUseCase( private val spacesRepository: SpacesRepository -): BaseUseCaseWithResult() { +): BaseUseCaseWithResult() { override fun run(params: Params) = spacesRepository.getSpaceMembers(params.accountName, params.spaceId) diff --git a/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt index f03d5cc97dc..935fe24aada 100644 --- a/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt +++ b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt @@ -33,8 +33,8 @@ import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.domain.spaces.model.OCSpace.Companion.SPACE_ID_SHARES import com.owncloud.android.domain.spaces.model.SpaceDeleted import com.owncloud.android.domain.spaces.model.SpaceFile -import com.owncloud.android.domain.spaces.model.SpaceMember -import com.owncloud.android.domain.spaces.model.SpaceMembers +import com.owncloud.android.domain.sharing.shares.model.MemberPermission +import com.owncloud.android.domain.sharing.shares.model.OCPermissions import com.owncloud.android.domain.spaces.model.SpaceOwner import com.owncloud.android.domain.spaces.model.SpaceQuota import com.owncloud.android.domain.spaces.model.SpaceRoot @@ -43,10 +43,10 @@ import com.owncloud.android.domain.spaces.model.SpaceSpecialFolder import com.owncloud.android.domain.spaces.model.SpaceUser import com.owncloud.android.lib.resources.roles.responses.RoleResponse import com.owncloud.android.lib.resources.spaces.responses.GrantedToV2Response -import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse +import com.owncloud.android.lib.resources.spaces.responses.MemberPermissionResponse import com.owncloud.android.lib.resources.spaces.responses.QuotaResponse import com.owncloud.android.lib.resources.spaces.responses.RootResponse -import com.owncloud.android.lib.resources.spaces.responses.SpacePermissionsResponse +import com.owncloud.android.lib.resources.spaces.responses.PermissionsResponse import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse import com.owncloud.android.lib.resources.spaces.responses.UserResponse @@ -320,7 +320,7 @@ val SPACE_RESPONSE = webDavUrl = "https://server.url/dav/spaces/8871f4f3-fc6f-4a66-8bed-62f175f76f3805bca744-d89f-4e9c-a990-25a0d7f03fe9", deleted = null, permissions = listOf( - PermissionsResponse( + MemberPermissionResponse( expirationDateTime = null, grantedToV2 = GrantedToV2Response(UserResponse(id = OC_USER_ID, displayName = OC_USER_INFO.displayName), null), id = null, @@ -340,7 +340,7 @@ val SPACE_RESPONSE = special = null, ) -val SPACE_PERMISSIONS_RESPONSE = SpacePermissionsResponse( +val SPACE_PERMISSIONS_RESPONSE = PermissionsResponse( actions = listOf("libre.graph/driveItem/permissions/delete", "libre.graph/driveItem/permissions/update"), roles = listOf( RoleResponse( @@ -360,7 +360,7 @@ val SPACE_PERMISSIONS_RESPONSE = SpacePermissionsResponse( ) ), members = listOf( - PermissionsResponse( + MemberPermissionResponse( expirationDateTime = null, grantedToV2 = GrantedToV2Response(UserResponse(id = OC_USER_ID, displayName = OC_USER_INFO.displayName), null), id = "u:$OC_USER_ID", @@ -372,7 +372,7 @@ val SPACE_PERMISSIONS_RESPONSE = SpacePermissionsResponse( ) ) -val SPACE_MEMBERS = SpaceMembers( +val SPACE_MEMBERS = OCPermissions( roles = listOf( OCRole( displayName = "Can view", @@ -391,13 +391,13 @@ val SPACE_MEMBERS = SpaceMembers( ) ), members = listOf( - SpaceMember( + MemberPermission( id = "u:$OC_USER_ID", expirationDateTime = null, displayName = OC_USER_INFO.displayName, roles = listOf("312c0871-5ef7-4b3a-85b6-0e4074c64049") ), - SpaceMember( + MemberPermission( id = "g:${OC_USER_GROUPS[0]}", expirationDateTime = "2025-07-03T12:09:43.364Z", displayName = "group-1",