Skip to content
7 changes: 7 additions & 0 deletions changelog/unreleased/4945
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -233,6 +234,7 @@ val useCaseModule = module {
factoryOf(::DeleteShareAsyncUseCase)
factoryOf(::EditPrivateShareAsyncUseCase)
factoryOf(::EditPublicShareAsyncUseCase)
factoryOf(::GetGraphSharesAsyncUseCase)
factoryOf(::GetShareAsLiveDataUseCase)
factoryOf(::GetShareesAsyncUseCase)
factoryOf(::GetSharesAsLiveDataUseCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -70,6 +71,7 @@ val viewModelModule = module {
viewModelOf(::DrawerViewModel)
viewModelOf(::FileDetailsViewModel)
viewModelOf(::FileOperationsViewModel)
viewModelOf(::GraphShareViewModel)
viewModelOf(::LogListViewModel)
viewModelOf(::OAuthViewModel)
viewModelOf(::PatternViewModel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<GraphShareViewModel> {
parametersOf(
requireArguments().getString(ARG_ACCOUNT_NAME),
requireArguments().getParcelable<OCFile>(ARG_FILE)
)
}

private lateinit var graphSharesAdapter: GraphSharesAdapter

private var roles: List<OCRole> = 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()
Comment thread
jesmrec marked this conversation as resolved.
}
}
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")
Comment thread
jesmrec marked this conversation as resolved.
}
}
}
}
}

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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<Event<UIResult<List<OCRole>>>?>(null)
val roles: StateFlow<Event<UIResult<List<OCRole>>>?> = _roles

private val _shares = MutableStateFlow<Event<UIResult<OCPermissions>>?>(null)
val shares: StateFlow<Event<UIResult<OCPermissions>>?> = _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
)
)
}
}
Loading