From 3698788369ead07a0f30449a9f28c47dbc702bb2 Mon Sep 17 00:00:00 2001 From: joragua Date: Fri, 26 Sep 2025 13:23:07 +0200 Subject: [PATCH 1/9] feat: add space edit option in bottom sheet dialog for users with proper permissions --- .../dependecyinjection/UseCaseModule.kt | 6 +- .../dependecyinjection/ViewModelModule.kt | 2 +- .../android/extensions/SpaceMenuOptionExt.kt | 34 ++++++ .../presentation/spaces/SpacesListFragment.kt | 109 ++++++++++++------ .../spaces/SpacesListViewModel.kt | 19 +++ owncloudApp/src/main/res/values/strings.xml | 1 + .../GetRemoteSpacePermissionsOperation.kt | 85 ++++++++++++++ .../spaces/services/OCSpacesService.kt | 4 + .../spaces/services/SpacesService.kt | 1 + .../datasources/RemoteSpacesDataSource.kt | 1 + .../OCRemoteSpacesDataSource.kt | 3 + .../spaces/repository/OCSpacesRepository.kt | 3 + .../android/domain/spaces/SpacesRepository.kt | 1 + .../domain/spaces/model/SpaceMenuOption.kt | 25 ++++ .../usecases/FilterSpaceMenuOptionsUseCase.kt | 61 ++++++++++ .../GetSpacePermissionsAsyncUseCase.kt | 33 ++++++ 16 files changed, 351 insertions(+), 37 deletions(-) create mode 100644 owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/SpaceMenuOption.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/FilterSpaceMenuOptionsUseCase.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpacePermissionsAsyncUseCase.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 933c09b47c9..76925e8c35a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -89,16 +89,18 @@ import com.owncloud.android.domain.sharing.shares.usecases.EditPublicShareAsyncU 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 +import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase +import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalSpaceForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetSpaceByIdForAccountUseCase +import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetSpacesFromEveryAccountUseCaseAsStream import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase -import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsStreamUseCase import com.owncloud.android.domain.transfers.usecases.GetAllTransfersUseCase @@ -222,12 +224,14 @@ val useCaseModule = module { // Spaces factoryOf(::CreateSpaceUseCase) + factoryOf(::FilterSpaceMenuOptionsUseCase) factoryOf(::GetPersonalAndProjectSpacesForAccountUseCase) factoryOf(::GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase) factoryOf(::GetPersonalSpaceForAccountUseCase) factoryOf(::GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase) factoryOf(::GetProjectSpacesWithSpecialsForAccountAsStreamUseCase) factoryOf(::GetSpaceByIdForAccountUseCase) + factoryOf(::GetSpacePermissionsAsyncUseCase) factoryOf(::GetSpaceWithSpecialsByIdForAccountUseCase) factoryOf(::GetSpacesFromEveryAccountUseCaseAsStream) factoryOf(::GetWebDavUrlForSpaceUseCase) 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 81e43a86123..0a6732c88bb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -102,6 +102,6 @@ val viewModelModule = module { get()) } viewModel { ReceiveExternalFilesViewModel(get(), get(), get(), get()) } viewModel { (accountName: String, showPersonalSpace: Boolean) -> - SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace) + SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt new file mode 100644 index 00000000000..892c75cb603 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt @@ -0,0 +1,34 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 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.extensions + +import com.owncloud.android.R +import com.owncloud.android.domain.spaces.model.SpaceMenuOption + +fun SpaceMenuOption.toStringResId() = + when (this) { + SpaceMenuOption.EDIT -> R.string.edit_space + } + +fun SpaceMenuOption.toDrawableResId() = + when (this) { + SpaceMenuOption.EDIT -> R.drawable.ic_pencil + } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt index 381cdd516cb..59905e01710 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt @@ -30,6 +30,7 @@ import android.view.MenuInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.SearchView +import androidx.core.content.res.ResourcesCompat import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -42,13 +43,17 @@ import com.owncloud.android.databinding.FileOptionsBottomSheetFragmentBinding import com.owncloud.android.databinding.SpacesListFragmentBinding import com.owncloud.android.domain.files.model.FileListOption import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.model.SpaceMenuOption import com.owncloud.android.extensions.collectLatestLifecycleFlow import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar import com.owncloud.android.extensions.toDrawableRes +import com.owncloud.android.extensions.toDrawableResId +import com.owncloud.android.extensions.toStringResId import com.owncloud.android.extensions.toSubtitleStringRes import com.owncloud.android.extensions.toTitleStringRes import com.owncloud.android.presentation.capabilities.CapabilityViewModel +import com.owncloud.android.presentation.common.BottomSheetFragmentItemView import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.presentation.common.UIResult import com.owncloud.android.presentation.spaces.createspace.CreateSpaceDialogFragment @@ -66,6 +71,8 @@ class SpacesListFragment : private val binding get() = _binding!! private var isMultiPersonal = false + private var editSpacesPermission: Boolean = false + private lateinit var currentSpace: OCSpace private val spacesListViewModel: SpacesListViewModel by viewModel { parametersOf( @@ -166,7 +173,10 @@ class SpacesListFragment : when (val uiResult = event.peekContent()) { is UIResult.Success -> { Timber.d("The permissions for $accountName are: ${uiResult.data}") - uiResult.data?.let { binding.fabCreateSpace.isVisible = it.contains(DRIVES_CREATE_ALL_PERMISSION) } + uiResult.data?.let { + binding.fabCreateSpace.isVisible = it.contains(DRIVES_CREATE_ALL_PERMISSION) + editSpacesPermission = it.contains(DRIVES_READ_WRITE_ALL_PERMISSION) + } } is UIResult.Loading -> { } is UIResult.Error -> { @@ -187,6 +197,47 @@ class SpacesListFragment : } } + collectLatestLifecycleFlow(spacesListViewModel.menuOptions) { menuOptions -> + val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater) + val dialog = BottomSheetDialog(requireContext()) + dialog.setContentView(spaceOptionsBottomSheetBinding.root) + + val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from( + spaceOptionsBottomSheetBinding.root.parent as View) + val closeBottomSheetButton = spaceOptionsBottomSheetBinding.closeBottomSheet + closeBottomSheetButton.setOnClickListener { + dialog.dismiss() + } + + val thumbnailBottomSheet = spaceOptionsBottomSheetBinding.thumbnailBottomSheet + thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space) + + val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet + spaceNameBottomSheet.text = currentSpace.name + + val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet + spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(currentSpace.quota?.used ?: 0L, requireContext(), true) + + val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet + spaceSeparatorBottomSheet.visibility = View.GONE + + menuOptions.forEach { menuOption -> + setMenuOption(menuOption, spaceOptionsBottomSheetBinding, dialog) + } + + fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_DRAGGING) { + fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + }) + + dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = spaceOptionsBottomSheetBinding.root.measuredHeight } + dialog.show() + } + } private fun showOrHideEmptyView(spacesList: List) { @@ -211,40 +262,8 @@ class SpacesListFragment : } override fun onThreeDotButtonClick(ocSpace: OCSpace) { - val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater) - val dialog = BottomSheetDialog(requireContext()) - dialog.setContentView(spaceOptionsBottomSheetBinding.root) - - val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from( - spaceOptionsBottomSheetBinding.root.parent as View) - val closeBottomSheetButton = spaceOptionsBottomSheetBinding.closeBottomSheet - closeBottomSheetButton.setOnClickListener { - dialog.dismiss() - } - - val thumbnailBottomSheet = spaceOptionsBottomSheetBinding.thumbnailBottomSheet - thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space) - - val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet - spaceNameBottomSheet.text = ocSpace.name - - val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet - spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(ocSpace.quota?.used ?: 0L, requireContext(), true) - - val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet - spaceSeparatorBottomSheet.visibility = View.GONE - - fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_DRAGGING) { - fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED - } - } - override fun onSlide(bottomSheet: View, slideOffset: Float) {} - }) - - dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = spaceOptionsBottomSheetBinding.root.measuredHeight } - dialog.show() + currentSpace = ocSpace + spacesListViewModel.filterMenuOptions(ocSpace, editSpacesPermission) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -276,12 +295,32 @@ class SpacesListFragment : searchViewRootToolbar.queryHint = getString(R.string.actionbar_search_space) } + private fun setMenuOption(menuOption: SpaceMenuOption, binding: FileOptionsBottomSheetFragmentBinding, dialog: BottomSheetDialog) { + val fileOptionItemView = BottomSheetFragmentItemView(requireContext()) + fileOptionItemView.apply { + itemIcon = ResourcesCompat.getDrawable(resources, menuOption.toDrawableResId(), null) + title = getString(menuOption.toStringResId()) + setOnClickListener { + dialog.dismiss() + when(menuOption) { + SpaceMenuOption.EDIT -> { + val accountName = requireArguments().getString(BUNDLE_ACCOUNT_NAME) + val editDialog = CreateSpaceDialogFragment.newInstance(accountName, this@SpacesListFragment) + editDialog.show(requireActivity().supportFragmentManager, DIALOG_CREATE_SPACE) + } + } + } + } + binding.fileOptionsBottomSheetLayout.addView(fileOptionItemView) + } + companion object { const val REQUEST_KEY_CLICK_SPACE = "REQUEST_KEY_CLICK_SPACE" const val BUNDLE_KEY_CLICK_SPACE = "BUNDLE_KEY_CLICK_SPACE" const val BUNDLE_SHOW_PERSONAL_SPACE = "showPersonalSpace" const val BUNDLE_ACCOUNT_NAME = "accountName" const val DRIVES_CREATE_ALL_PERMISSION = "Drives.Create.all" + const val DRIVES_READ_WRITE_ALL_PERMISSION = "Drives.ReadWrite.all" private const val DIALOG_CREATE_SPACE = "DIALOG_CREATE_SPACE" diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt index cca765f7e39..e866e699b2a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt @@ -29,7 +29,9 @@ import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PATH import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.model.SpaceMenuOption import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase +import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase @@ -57,6 +59,7 @@ class SpacesListViewModel( private val getUserIdAsyncUseCase: GetUserIdAsyncUseCase, private val getUserPermissionsAsyncUseCase: GetUserPermissionsAsyncUseCase, private val createSpaceUseCase: CreateSpaceUseCase, + private val filterSpaceMenuOptionsUseCase: FilterSpaceMenuOptionsUseCase, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, private val accountName: String, private val showPersonalSpace: Boolean, @@ -72,6 +75,9 @@ class SpacesListViewModel( private val _userPermissions = MutableStateFlow>>?>(null) val userPermissions: StateFlow>>?> = _userPermissions + private val _menuOptions: MutableSharedFlow> = MutableSharedFlow() + val menuOptions: SharedFlow> = _menuOptions + private val _createSpaceFlow = MutableSharedFlow>?>() val createSpaceFlow: SharedFlow>?> = _createSpaceFlow @@ -155,6 +161,19 @@ class SpacesListViewModel( } } + fun filterMenuOptions(space: OCSpace, editSpacesPermission: Boolean) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = filterSpaceMenuOptionsUseCase( + FilterSpaceMenuOptionsUseCase.Params( + accountName = accountName, + space = space, + editSpacesPermission = editSpacesPermission + ) + ) + _menuOptions.emit(result) + } + } + data class SpacesListUiState( val spaces: List, val rootFolderFromSelectedSpace: OCFile? = null, diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 943d5d95045..bd30354538a 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -851,6 +851,7 @@ Forbidden characters: / \\ . : ? * " ' > < | Space created correctly Space could not be created + Edit space forum or contribute in our GitHub repo]]> 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 new file mode 100644 index 00000000000..130eb6bb661 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacePermissionsOperation.kt @@ -0,0 +1,85 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 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.spaces + +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.squareup.moshi.Json +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import timber.log.Timber +import java.net.URL + +class GetRemoteSpacePermissionsOperation( + private val spaceId: String +): RemoteOperation>() { + override fun run(client: OwnCloudClient): RemoteOperationResult> { + var result: RemoteOperationResult> + try { + val requestUri = client.baseUri.buildUpon().apply { + appendEncodedPath(GRAPH_API_SPACES_PATH) + appendEncodedPath(spaceId) + appendEncodedPath(SPACE_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(SpacePermissionsListResponse::class.java) + + result = RemoteOperationResult(ResultCode.OK) + result.data = getMethod.getResponseBodyAsString().let { adapter.fromJson(it)?.permissions ?: emptyList() } + + Timber.d("Get space permissions for user completed and parsed to ${result.data}") + } else { + result = RemoteOperationResult(getMethod) + Timber.e("Failed response while getting space permissions; status code: $status, response: $response") + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Timber.e(e, "Exception while getting space permissions for user") + } + return result + } + + @JsonClass(generateAdapter = true) + data class SpacePermissionsListResponse( + @Json(name = "@libre.graph.permissions.actions.allowedValues") + val permissions: List + ) + + companion object { + private const val GRAPH_API_SPACES_PATH = "graph/v1beta1/drives/" + private const val SPACE_PERMISSIONS_ENDPOINT = "root/permissions" + } +} 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 0bf79b91e59..d51c0e02f2c 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 @@ -25,6 +25,7 @@ package com.owncloud.android.lib.resources.spaces.services import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.spaces.CreateRemoteSpaceOperation +import com.owncloud.android.lib.resources.spaces.GetRemoteSpacePermissionsOperation import com.owncloud.android.lib.resources.spaces.GetRemoteSpacesOperation import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse @@ -35,4 +36,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> = + GetRemoteSpacePermissionsOperation(spaceId).execute(client) + } 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 63875260a71..2d445f977e0 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 @@ -29,4 +29,5 @@ 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> } 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 949940d55c2..1ccefe938ee 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 @@ -25,4 +25,5 @@ import com.owncloud.android.domain.spaces.model.OCSpace interface RemoteSpacesDataSource { fun refreshSpacesForAccount(accountName: String): List fun createSpace(accountName: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long): OCSpace + fun getSpacePermissions(accountName: String, spaceId: String): List } 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 a1b6f719e49..ca8ab6c5ad9 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 @@ -53,6 +53,9 @@ class OCRemoteSpacesDataSource( return spaceResponse.toModel(accountName) } + override fun getSpacePermissions(accountName: String, spaceId: String): List = + executeRemoteOperation { clientManager.getSpacesService(accountName).getSpacePermissions(spaceId) } + companion object { @VisibleForTesting 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 e7a74779e36..3c2b20b9fc2 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 @@ -74,6 +74,9 @@ class OCSpacesRepository( override fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace? = localSpacesDataSource.getSpaceByIdForAccount(spaceId = spaceId, accountName = accountName) + override fun getSpacePermissions(accountName: String, spaceId: String): List = + remoteSpacesDataSource.getSpacePermissions(accountName, spaceId) + override fun getWebDavUrlForSpace(accountName: String, spaceId: String?): String? = localSpacesDataSource.getWebDavUrlForSpace(accountName = accountName, spaceId = spaceId) 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 36c6d72c21f..1450701f01b 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 @@ -33,6 +33,7 @@ interface SpacesRepository { fun getPersonalAndProjectSpacesForAccount(accountName: String): List fun getSpaceWithSpecialsByIdForAccount(spaceId: String?, accountName: String): OCSpace fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace? + 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/SpaceMenuOption.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/SpaceMenuOption.kt new file mode 100644 index 00000000000..75c73510a0b --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/SpaceMenuOption.kt @@ -0,0 +1,25 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 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.spaces.model + +enum class SpaceMenuOption { + EDIT +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/FilterSpaceMenuOptionsUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/FilterSpaceMenuOptionsUseCase.kt new file mode 100644 index 00000000000..39e9172d280 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/FilterSpaceMenuOptionsUseCase.kt @@ -0,0 +1,61 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 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.spaces.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.UseCaseResult +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.model.SpaceMenuOption + +class FilterSpaceMenuOptionsUseCase( + private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase, +) : BaseUseCase, FilterSpaceMenuOptionsUseCase.Params>() { + + override fun run(params: Params): MutableList { + val optionsToShow = mutableListOf() + + val editPermission = if (params.editSpacesPermission) { + true + } else { + when (val spacePermissionsResult = + getSpacePermissionsAsyncUseCase(GetSpacePermissionsAsyncUseCase.Params(params.accountName, params.space.id))) { + is UseCaseResult.Success -> DRIVES_MANAGE_PERMISSION in spacePermissionsResult.data + is UseCaseResult.Error -> false + } + } + + if (editPermission) { + optionsToShow.add(SpaceMenuOption.EDIT) + } + + return optionsToShow + } + + data class Params( + val accountName: String, + val space: OCSpace, + val editSpacesPermission: Boolean + ) + + companion object { + private const val DRIVES_MANAGE_PERMISSION = "libre.graph/driveItem/permissions/update" + } +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpacePermissionsAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpacePermissionsAsyncUseCase.kt new file mode 100644 index 00000000000..ba902be1c66 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetSpacePermissionsAsyncUseCase.kt @@ -0,0 +1,33 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 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.spaces.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.spaces.SpacesRepository + +class GetSpacePermissionsAsyncUseCase( + private val spacesRepository: SpacesRepository +): BaseUseCaseWithResult, GetSpacePermissionsAsyncUseCase.Params>() { + + override fun run(params: Params) = spacesRepository.getSpacePermissions(params.accountName, params.spaceId) + + data class Params(val accountName: String, val spaceId: String) +} From 7a9dca108c54c7cf8aae294a806c7cff5120480f Mon Sep 17 00:00:00 2001 From: joragua Date: Fri, 3 Oct 2025 14:07:16 +0200 Subject: [PATCH 2/9] feat: update create space dialog for edit mode --- .../presentation/spaces/SpacesListFragment.kt | 20 ++++++++--- .../createspace/CreateSpaceDialogFragment.kt | 36 +++++++++++++++++-- .../main/res/layout/create_space_dialog.xml | 2 ++ .../android/domain/spaces/model/OCSpace.kt | 33 +++++++++++------ 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt index 59905e01710..1c474a22b18 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt @@ -71,7 +71,8 @@ class SpacesListFragment : private val binding get() = _binding!! private var isMultiPersonal = false - private var editSpacesPermission: Boolean = false + private var editSpacesPermission = false + private var editQuotaPermission = false private lateinit var currentSpace: OCSpace private val spacesListViewModel: SpacesListViewModel by viewModel { @@ -119,7 +120,12 @@ class SpacesListFragment : } binding.fabCreateSpace.setOnClickListener { - val dialog = CreateSpaceDialogFragment.newInstance(requireArguments().getString(BUNDLE_ACCOUNT_NAME), this) + val dialog = CreateSpaceDialogFragment.newInstance( + isEditMode = false, + canEditQuota = false, + currentSpace = null, + listener = this + ) dialog.show(requireActivity().supportFragmentManager, DIALOG_CREATE_SPACE) binding.fabCreateSpace.isFocusable = false } @@ -176,6 +182,7 @@ class SpacesListFragment : uiResult.data?.let { binding.fabCreateSpace.isVisible = it.contains(DRIVES_CREATE_ALL_PERMISSION) editSpacesPermission = it.contains(DRIVES_READ_WRITE_ALL_PERMISSION) + editQuotaPermission = it.contains(DRIVES_READ_WRITE_PROJECT_QUOTA_ALL_PERMISSION) } } is UIResult.Loading -> { } @@ -304,8 +311,12 @@ class SpacesListFragment : dialog.dismiss() when(menuOption) { SpaceMenuOption.EDIT -> { - val accountName = requireArguments().getString(BUNDLE_ACCOUNT_NAME) - val editDialog = CreateSpaceDialogFragment.newInstance(accountName, this@SpacesListFragment) + val editDialog = CreateSpaceDialogFragment.newInstance( + isEditMode = true, + canEditQuota = editQuotaPermission, + currentSpace = currentSpace, + listener = this@SpacesListFragment + ) editDialog.show(requireActivity().supportFragmentManager, DIALOG_CREATE_SPACE) } } @@ -321,6 +332,7 @@ class SpacesListFragment : const val BUNDLE_ACCOUNT_NAME = "accountName" const val DRIVES_CREATE_ALL_PERMISSION = "Drives.Create.all" const val DRIVES_READ_WRITE_ALL_PERMISSION = "Drives.ReadWrite.all" + const val DRIVES_READ_WRITE_PROJECT_QUOTA_ALL_PERMISSION = "Drives.ReadWriteProjectQuota.all" private const val DIALOG_CREATE_SPACE = "DIALOG_CREATE_SPACE" diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt index 0196cd87fc7..f793633aed9 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt @@ -24,10 +24,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.DialogFragment import com.owncloud.android.R import com.owncloud.android.databinding.CreateSpaceDialogBinding +import com.owncloud.android.domain.spaces.model.OCSpace class CreateSpaceDialogFragment : DialogFragment() { private var _binding: CreateSpaceDialogBinding? = null @@ -44,12 +46,31 @@ class CreateSpaceDialogFragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val currentSpace = requireArguments().getParcelable(ARG_CURRENT_SPACE) + val isEditMode = requireArguments().getBoolean(ARG_EDIT_SPACE_MODE) binding.apply { cancelCreateSpaceButton.setOnClickListener { dialog?.dismiss() } createSpaceDialogNameValue.doOnTextChanged { name, _, _, _ -> val errorMessage = validateName(name.toString()) updateUI(errorMessage) } + + if (isEditMode) { + createSpaceDialogTitle.text = getString(R.string.edit_space) + val canEditQuota = requireArguments().getBoolean(ARG_CAN_EDIT_SPACE_QUOTA) + createSpaceDialogQuotaSection.isVisible = canEditQuota + + currentSpace?.let { + createSpaceDialogNameValue.setText(it.name) + createSpaceDialogSubtitleValue.setText(it.description) + } + + createSpaceButton.apply { + text = getString(R.string.share_confirm_public_link_button) + contentDescription = getString(R.string.share_confirm_public_link_button) + } + } + createSpaceButton.setOnClickListener { val spaceQuota = convertToBytes(binding.createSpaceDialogQuotaUnit.selectedItem.toString()) createSpaceListener.createSpace( @@ -94,12 +115,21 @@ class CreateSpaceDialogFragment : DialogFragment() { } companion object { - private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME" + private const val ARG_EDIT_SPACE_MODE = "EDIT_SPACE_MODE" + private const val ARG_CAN_EDIT_SPACE_QUOTA = "CAN_EDIT_SPACE_QUOTA" + private const val ARG_CURRENT_SPACE = "CURRENT_SPACE" private const val FORBIDDEN_CHARACTERS = """[/\\.:?*"'><|]""" - fun newInstance(accountName: String?, listener: CreateSpaceListener): CreateSpaceDialogFragment { + fun newInstance( + isEditMode: Boolean, + canEditQuota: Boolean, + currentSpace: OCSpace?, + listener: CreateSpaceListener + ): CreateSpaceDialogFragment { val args = Bundle().apply { - putString(ARG_ACCOUNT_NAME, accountName) + putBoolean(ARG_EDIT_SPACE_MODE, isEditMode) + putBoolean(ARG_CAN_EDIT_SPACE_QUOTA, canEditQuota) + putParcelable(ARG_CURRENT_SPACE, currentSpace) } return CreateSpaceDialogFragment().apply { createSpaceListener = listener diff --git a/owncloudApp/src/main/res/layout/create_space_dialog.xml b/owncloudApp/src/main/res/layout/create_space_dialog.xml index f8f8f2196db..67dbaf73999 100644 --- a/owncloudApp/src/main/res/layout/create_space_dialog.xml +++ b/owncloudApp/src/main/res/layout/create_space_dialog.xml @@ -23,6 +23,7 @@ @@ -119,6 +120,7 @@ ?, -) { +) : Parcelable { val isPersonal get() = driveType == DRIVE_TYPE_PERSONAL val isProject get() = driveType == DRIVE_TYPE_PROJECT @@ -57,24 +62,28 @@ data class OCSpace( } } +@Parcelize data class SpaceOwner( val user: SpaceUser -) +) : Parcelable +@Parcelize data class SpaceQuota( val remaining: Long?, val state: String?, val total: Long, val used: Long?, -) +) : Parcelable +@Parcelize data class SpaceRoot( val eTag: String?, val id: String, val webDavUrl: String, val deleted: SpaceDeleted?, -) +) : Parcelable +@Parcelize data class SpaceSpecial( val eTag: String, val file: SpaceFile, @@ -84,20 +93,24 @@ data class SpaceSpecial( val size: Int, val specialFolder: SpaceSpecialFolder, val webDavUrl: String -) +) : Parcelable +@Parcelize data class SpaceDeleted( val state: String, -) +) : Parcelable +@Parcelize data class SpaceUser( val id: String -) +) : Parcelable +@Parcelize data class SpaceFile( val mimeType: String -) +) : Parcelable +@Parcelize data class SpaceSpecialFolder( val name: String -) +) : Parcelable From 9c9d34294cae708e081f007c7b1a0e4c54e4263c Mon Sep 17 00:00:00 2001 From: joragua Date: Fri, 3 Oct 2025 14:08:01 +0200 Subject: [PATCH 3/9] feat: implement methods and network operation to update a space on the server --- .../dependecyinjection/UseCaseModule.kt | 2 + .../dependecyinjection/ViewModelModule.kt | 2 +- .../presentation/spaces/SpacesListFragment.kt | 92 +++++++++------- .../spaces/SpacesListViewModel.kt | 15 +++ .../createspace/CreateSpaceDialogFragment.kt | 27 +++-- owncloudApp/src/main/res/values/strings.xml | 2 + .../http/methods/nonwebdav/PatchMethod.kt | 48 +++++++++ .../spaces/EditRemoteSpaceOperation.kt | 100 ++++++++++++++++++ .../spaces/services/OCSpacesService.kt | 4 + .../spaces/services/SpacesService.kt | 1 + .../datasources/RemoteSpacesDataSource.kt | 1 + .../OCRemoteSpacesDataSource.kt | 7 ++ .../spaces/repository/OCSpacesRepository.kt | 4 + .../android/domain/spaces/SpacesRepository.kt | 1 + .../spaces/usecases/EditSpaceUseCase.kt | 41 +++++++ 15 files changed, 302 insertions(+), 45 deletions(-) create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/nonwebdav/PatchMethod.kt create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/EditRemoteSpaceOperation.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/EditSpaceUseCase.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 76925e8c35a..94e55d086f6 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -90,6 +90,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUse import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase +import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase @@ -224,6 +225,7 @@ val useCaseModule = module { // Spaces factoryOf(::CreateSpaceUseCase) + factoryOf(::EditSpaceUseCase) factoryOf(::FilterSpaceMenuOptionsUseCase) factoryOf(::GetPersonalAndProjectSpacesForAccountUseCase) factoryOf(::GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase) 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 0a6732c88bb..72bcb6c6040 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -102,6 +102,6 @@ val viewModelModule = module { get()) } viewModel { ReceiveExternalFilesViewModel(get(), get(), get(), get()) } viewModel { (accountName: String, showPersonalSpace: Boolean) -> - SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace) + SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt index 1c474a22b18..1f1af976183 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt @@ -204,45 +204,18 @@ class SpacesListFragment : } } - collectLatestLifecycleFlow(spacesListViewModel.menuOptions) { menuOptions -> - val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater) - val dialog = BottomSheetDialog(requireContext()) - dialog.setContentView(spaceOptionsBottomSheetBinding.root) - - val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from( - spaceOptionsBottomSheetBinding.root.parent as View) - val closeBottomSheetButton = spaceOptionsBottomSheetBinding.closeBottomSheet - closeBottomSheetButton.setOnClickListener { - dialog.dismiss() - } - - val thumbnailBottomSheet = spaceOptionsBottomSheetBinding.thumbnailBottomSheet - thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space) - - val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet - spaceNameBottomSheet.text = currentSpace.name - - val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet - spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(currentSpace.quota?.used ?: 0L, requireContext(), true) - - val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet - spaceSeparatorBottomSheet.visibility = View.GONE - - menuOptions.forEach { menuOption -> - setMenuOption(menuOption, spaceOptionsBottomSheetBinding, dialog) - } - - fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_DRAGGING) { - fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED - } + collectLatestLifecycleFlow(spacesListViewModel.editSpaceFlow) { event -> + event?.let { + when (val uiResult = event.peekContent()) { + is UIResult.Success -> { showMessageInSnackbar(getString(R.string.edit_space_correctly)) } + is UIResult.Loading -> { } + is UIResult.Error -> { showErrorInSnackbar(R.string.edit_space_failed, uiResult.error) } } - override fun onSlide(bottomSheet: View, slideOffset: Float) {} - }) + } + } - dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = spaceOptionsBottomSheetBinding.root.measuredHeight } - dialog.show() + collectLatestLifecycleFlow(spacesListViewModel.menuOptions) { menuOptions -> + showSpaceMenuOptionsDialog(menuOptions) } } @@ -293,6 +266,10 @@ class SpacesListFragment : spacesListViewModel.createSpace(spaceName, spaceSubtitle, spaceQuota) } + override fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) { + spacesListViewModel.editSpace(spaceId, spaceName, spaceSubtitle, spaceQuota) + } + fun setSearchListener(searchView: SearchView) { searchView.setOnQueryTextListener(this) } @@ -302,6 +279,47 @@ class SpacesListFragment : searchViewRootToolbar.queryHint = getString(R.string.actionbar_search_space) } + private fun showSpaceMenuOptionsDialog(menuOptions: List) { + val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater) + val dialog = BottomSheetDialog(requireContext()) + dialog.setContentView(spaceOptionsBottomSheetBinding.root) + + val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from( + spaceOptionsBottomSheetBinding.root.parent as View) + val closeBottomSheetButton = spaceOptionsBottomSheetBinding.closeBottomSheet + closeBottomSheetButton.setOnClickListener { + dialog.dismiss() + } + + val thumbnailBottomSheet = spaceOptionsBottomSheetBinding.thumbnailBottomSheet + thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space) + + val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet + spaceNameBottomSheet.text = currentSpace.name + + val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet + spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(currentSpace.quota?.used ?: 0L, requireContext(), true) + + val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet + spaceSeparatorBottomSheet.visibility = View.GONE + + menuOptions.forEach { menuOption -> + setMenuOption(menuOption, spaceOptionsBottomSheetBinding, dialog) + } + + fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_DRAGGING) { + fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + }) + + dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = spaceOptionsBottomSheetBinding.root.measuredHeight } + dialog.show() + } + private fun setMenuOption(menuOption: SpaceMenuOption, binding: FileOptionsBottomSheetFragmentBinding, dialog: BottomSheetDialog) { val fileOptionItemView = BottomSheetFragmentItemView(requireContext()) fileOptionItemView.apply { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt index e866e699b2a..5eb9b52c9bf 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt @@ -31,6 +31,7 @@ import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.domain.spaces.model.SpaceMenuOption import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase +import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase @@ -60,6 +61,7 @@ class SpacesListViewModel( private val getUserPermissionsAsyncUseCase: GetUserPermissionsAsyncUseCase, private val createSpaceUseCase: CreateSpaceUseCase, private val filterSpaceMenuOptionsUseCase: FilterSpaceMenuOptionsUseCase, + private val editSpaceUseCase: EditSpaceUseCase, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, private val accountName: String, private val showPersonalSpace: Boolean, @@ -81,6 +83,9 @@ class SpacesListViewModel( private val _createSpaceFlow = MutableSharedFlow>?>() val createSpaceFlow: SharedFlow>?> = _createSpaceFlow + private val _editSpaceFlow = MutableSharedFlow>?>() + val editSpaceFlow: SharedFlow>?> = _editSpaceFlow + init { viewModelScope.launch(coroutinesDispatcherProvider.io) { refreshSpacesFromServer() @@ -161,6 +166,16 @@ class SpacesListViewModel( } } + fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + when (val result = editSpaceUseCase(EditSpaceUseCase.Params(accountName, spaceId, spaceName, spaceSubtitle, spaceQuota))) { + is UseCaseResult.Success -> _editSpaceFlow.emit(Event(UIResult.Success(result.getDataOrNull()))) + is UseCaseResult.Error -> _editSpaceFlow.emit(Event(UIResult.Error(error = result.getThrowableOrNull()))) + } + refreshSpacesFromServerAsyncUseCase(RefreshSpacesFromServerAsyncUseCase.Params(accountName)) + } + } + fun filterMenuOptions(space: OCSpace, editSpacesPermission: Boolean) { viewModelScope.launch(coroutinesDispatcherProvider.io) { val result = filterSpaceMenuOptionsUseCase( diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt index f793633aed9..13e6b2e43a1 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt @@ -48,6 +48,7 @@ class CreateSpaceDialogFragment : DialogFragment() { super.onViewCreated(view, savedInstanceState) val currentSpace = requireArguments().getParcelable(ARG_CURRENT_SPACE) val isEditMode = requireArguments().getBoolean(ARG_EDIT_SPACE_MODE) + val canEditQuota = requireArguments().getBoolean(ARG_CAN_EDIT_SPACE_QUOTA) binding.apply { cancelCreateSpaceButton.setOnClickListener { dialog?.dismiss() } createSpaceDialogNameValue.doOnTextChanged { name, _, _, _ -> @@ -57,7 +58,6 @@ class CreateSpaceDialogFragment : DialogFragment() { if (isEditMode) { createSpaceDialogTitle.text = getString(R.string.edit_space) - val canEditQuota = requireArguments().getBoolean(ARG_CAN_EDIT_SPACE_QUOTA) createSpaceDialogQuotaSection.isVisible = canEditQuota currentSpace?.let { @@ -72,12 +72,24 @@ class CreateSpaceDialogFragment : DialogFragment() { } createSpaceButton.setOnClickListener { - val spaceQuota = convertToBytes(binding.createSpaceDialogQuotaUnit.selectedItem.toString()) - createSpaceListener.createSpace( - spaceName = binding.createSpaceDialogNameValue.text.toString(), - spaceSubtitle = binding.createSpaceDialogSubtitleValue.text.toString(), - spaceQuota = spaceQuota - ) + val spaceName = createSpaceDialogNameValue.text.toString() + val spaceSubtitle = createSpaceDialogSubtitleValue.text.toString() + val spaceQuota = convertToBytes(createSpaceDialogQuotaUnit.selectedItem.toString()) + + if (isEditMode) { + createSpaceListener.editSpace( + spaceId = currentSpace!!.id, + spaceName = spaceName, + spaceSubtitle = spaceSubtitle, + spaceQuota = if (canEditQuota) spaceQuota else null + ) + } else { + createSpaceListener.createSpace( + spaceName = spaceName, + spaceSubtitle = spaceSubtitle, + spaceQuota = spaceQuota + ) + } dialog?.dismiss() } } @@ -112,6 +124,7 @@ class CreateSpaceDialogFragment : DialogFragment() { interface CreateSpaceListener { fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long) + fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) } companion object { diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index bd30354538a..cd634783ae2 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -852,6 +852,8 @@ Space created correctly Space could not be created Edit space + Space updated correctly + Space could not be updated forum or contribute in our GitHub repo]]> diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/nonwebdav/PatchMethod.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/nonwebdav/PatchMethod.kt new file mode 100644 index 00000000000..1843e0a6953 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/nonwebdav/PatchMethod.kt @@ -0,0 +1,48 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2025 ownCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package com.owncloud.android.lib.common.http.methods.nonwebdav + +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import java.io.IOException +import java.net.URL + +/** + * OkHttp patch calls wrapper + * + * @author Jorge Aguado Recio + */ +class PatchMethod( + url: URL, + private val patchRequestBody: RequestBody +) : HttpMethod(url) { + @Throws(IOException::class) + override fun onExecute(okHttpClient: OkHttpClient): Int { + request = request.newBuilder() + .patch(patchRequestBody) + .build() + return super.onExecute(okHttpClient) + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/EditRemoteSpaceOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/EditRemoteSpaceOperation.kt new file mode 100644 index 00000000000..90c50ea4516 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/EditRemoteSpaceOperation.kt @@ -0,0 +1,100 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 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.spaces + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_JSON +import com.owncloud.android.lib.common.http.methods.nonwebdav.PatchMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import timber.log.Timber +import java.net.URL + +class EditRemoteSpaceOperation( + private val spaceId: String, + private val spaceName: String, + private val spaceSubtitle: String, + private val spaceQuota: Long? +): RemoteOperation() { + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult + try { + val moshi = Moshi.Builder().build() + + val uriBuilder = client.baseUri.buildUpon().apply { + appendEncodedPath(GRAPH_API_SPACES_PATH) + appendEncodedPath(spaceId) + } + + val requestBody = JSONObject().apply { + put(SPACE_NAME_BODY_PARAM, spaceName) + spaceQuota?.let { + put(SPACE_QUOTA_BODY_PARAM, JSONObject().apply { + put(SPACE_QUOTA_TOTAL_BODY_PARAM, spaceQuota) + }) + } + put(SPACE_DESCRIPTION_BODY_PARAM, spaceSubtitle) + }.toString().toRequestBody(CONTENT_TYPE_JSON.toMediaType()) + + val patchMethod = PatchMethod(URL(uriBuilder.build().toString()), requestBody) + + val status = client.executeHttpMethod(patchMethod) + + val response = patchMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_OK) { + Timber.d("Successful response: $response") + + val responseAdapter: JsonAdapter = moshi.adapter(SpaceResponse::class.java) + + result = RemoteOperationResult(ResultCode.OK) + result.data = responseAdapter.fromJson(response) + + Timber.d("Update of space completed and parsed to ${result.data}") + } else { + result = RemoteOperationResult(patchMethod) + Timber.e("Failed response while updating the space; status code: $status, response: $response") + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Timber.e(e, "Exception while updating the space $spaceId") + } + return result + } + + companion object { + private const val GRAPH_API_SPACES_PATH = "graph/v1.0/drives/" + private const val SPACE_NAME_BODY_PARAM = "name" + private const val SPACE_QUOTA_BODY_PARAM = "quota" + private const val SPACE_QUOTA_TOTAL_BODY_PARAM = "total" + private const val SPACE_DESCRIPTION_BODY_PARAM = "description" + } + +} 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 d51c0e02f2c..3a60f427ad2 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 @@ -25,6 +25,7 @@ package com.owncloud.android.lib.resources.spaces.services import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.spaces.CreateRemoteSpaceOperation +import com.owncloud.android.lib.resources.spaces.EditRemoteSpaceOperation import com.owncloud.android.lib.resources.spaces.GetRemoteSpacePermissionsOperation import com.owncloud.android.lib.resources.spaces.GetRemoteSpacesOperation import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse @@ -39,4 +40,7 @@ class OCSpacesService(override val client: OwnCloudClient) : SpacesService { override fun getSpacePermissions(spaceId: String): RemoteOperationResult> = GetRemoteSpacePermissionsOperation(spaceId).execute(client) + override fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult = + EditRemoteSpaceOperation(spaceId, spaceName, spaceSubtitle, spaceQuota).execute(client) + } 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 2d445f977e0..ba1bc0dc268 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 @@ -30,4 +30,5 @@ interface SpacesService : Service { fun getSpaces(): RemoteOperationResult> fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long): RemoteOperationResult fun getSpacePermissions(spaceId: String): RemoteOperationResult> + fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult } 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 1ccefe938ee..eb7a2673104 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 @@ -26,4 +26,5 @@ interface RemoteSpacesDataSource { fun refreshSpacesForAccount(accountName: String): List fun createSpace(accountName: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long): OCSpace fun getSpacePermissions(accountName: String, spaceId: String): List + fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): 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 ca8ab6c5ad9..582a58e7273 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 @@ -56,6 +56,13 @@ class OCRemoteSpacesDataSource( override fun getSpacePermissions(accountName: String, spaceId: String): List = executeRemoteOperation { clientManager.getSpacesService(accountName).getSpacePermissions(spaceId) } + override fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): OCSpace { + val spaceResponse = executeRemoteOperation { + clientManager.getSpacesService(accountName).editSpace(spaceId, spaceName, spaceSubtitle, spaceQuota) + } + return spaceResponse.toModel(accountName) + } + companion object { @VisibleForTesting 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 3c2b20b9fc2..f8b5ebccde1 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 @@ -84,4 +84,8 @@ class OCSpacesRepository( remoteSpacesDataSource.createSpace(accountName, spaceName, spaceSubtitle, spaceQuota) } + override fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) { + remoteSpacesDataSource.editSpace(accountName, spaceId, spaceName, spaceSubtitle, spaceQuota) + } + } 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 1450701f01b..5f5482b62e2 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 @@ -36,4 +36,5 @@ interface SpacesRepository { fun getSpacePermissions(accountName: String, spaceId: String): List fun getWebDavUrlForSpace(accountName: String, spaceId: String?): String? fun createSpace(accountName: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long) + fun editSpace(accountName: String, spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/EditSpaceUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/EditSpaceUseCase.kt new file mode 100644 index 00000000000..fe297719ac2 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/EditSpaceUseCase.kt @@ -0,0 +1,41 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2025 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.spaces.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.spaces.SpacesRepository + +class EditSpaceUseCase( + private val spacesRepository: SpacesRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Params) = + spacesRepository.editSpace(params.accountName, params.spaceId, params.spaceName, params.spaceSubtitle, params.spaceQuota) + + data class Params( + val accountName: String, + val spaceId: String, + val spaceName: String, + val spaceSubtitle: String, + val spaceQuota: Long? + ) + +} From f30a3cef9e362b174169f0a0c81264c0bbe523e5 Mon Sep 17 00:00:00 2001 From: joragua Date: Mon, 6 Oct 2025 14:03:16 +0200 Subject: [PATCH 4/9] feat: show the corresponding quota value in the dialog --- .../presentation/spaces/SpacesListDiffUtil.kt | 5 +++-- .../createspace/CreateSpaceDialogFragment.kt | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt index 532c59f5a0a..68102fafdd3 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt @@ -2,8 +2,9 @@ * ownCloud Android client application * * @author Juan Carlos Garrote Gascón + * @author Jorge Aguado Recio * - * Copyright (C) 2023 ownCloud GmbH. + * Copyright (C) 2025 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, @@ -42,7 +43,7 @@ class SpacesListDiffUtil( val oldItem = oldList[oldItemPosition] val newItem = newList[newItemPosition] - if ((oldItem.name != newItem.name) || (oldItem.description != newItem.description) || + if ((oldItem.name != newItem.name) || (oldItem.description != newItem.description) || (oldItem.quota != newItem.quota) || (oldItem.getSpaceSpecialImage()?.id != newItem.getSpaceSpecialImage()?.id)) { return false } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt index 13e6b2e43a1..96340e0bb84 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt @@ -63,6 +63,7 @@ class CreateSpaceDialogFragment : DialogFragment() { currentSpace?.let { createSpaceDialogNameValue.setText(it.name) createSpaceDialogSubtitleValue.setText(it.description) + createSpaceDialogQuotaUnit.setSelection(getQuotaValueForSpinner(it.quota!!.total)) } createSpaceButton.apply { @@ -122,6 +123,20 @@ class CreateSpaceDialogFragment : DialogFragment() { return quotaNumber * 1_000_000_000L } + private fun getQuotaValueForSpinner(spaceQuota: Long): Int { + val totalGB = spaceQuota / 1_000_000_000.0 + return when (totalGB) { + 1.0 -> 0 + 2.0-> 1 + 5.0 -> 2 + 10.0 -> 3 + 50.0 -> 4 + 100.0 -> 5 + 0.0 -> 6 + else -> 0 + } + } + interface CreateSpaceListener { fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long) fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) From 278440bedb5dd9c64bbb6f411b6828c145d0488b Mon Sep 17 00:00:00 2001 From: joragua Date: Mon, 6 Oct 2025 14:55:49 +0200 Subject: [PATCH 5/9] test: add new tests for OCRemoteSpacesDataSourceTest and OCSpacesRepositoryTest --- .../OCRemoteSpacesDataSourceTest.kt | 54 ++++++++++++++++++- .../repository/OCSpacesRepositoryTest.kt | 46 ++++++++++++++++ .../com/owncloud/android/testutil/OCSpace.kt | 5 ++ 3 files changed, 104 insertions(+), 1 deletion(-) 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 b7263b9bcff..0bbd47f412d 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 @@ -3,8 +3,9 @@ * * @author Aitor Ballesteros Pavón * @author Juan Carlos Garrote Gascón + * @author Jorge Aguado Recio * - * Copyright (C) 2023 ownCloud GmbH. + * Copyright (C) 2025 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, @@ -26,6 +27,7 @@ import com.owncloud.android.data.spaces.datasources.implementation.OCRemoteSpace import com.owncloud.android.lib.resources.spaces.services.OCSpacesService import com.owncloud.android.testutil.OC_ACCOUNT_NAME import com.owncloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE +import com.owncloud.android.testutil.SPACE_PERMISSIONS import com.owncloud.android.testutil.SPACE_RESPONSE import com.owncloud.android.utils.createRemoteOperationResultMock import io.mockk.every @@ -96,4 +98,54 @@ class OCRemoteSpacesDataSourceTest { } } + @Test + fun `getSpacePermissions returns a list of String with project space permissions`() { + val getSpacePermissionsResult = createRemoteOperationResultMock(SPACE_PERMISSIONS, isSuccess = true) + + every { + ocSpaceService.getSpacePermissions(OC_SPACE_PROJECT_WITH_IMAGE.id) + } returns getSpacePermissionsResult + + val spacePermissions = ocRemoteSpacesDataSource.getSpacePermissions(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) + assertEquals(SPACE_PERMISSIONS, spacePermissions) + + verify(exactly = 1) { + clientManager.getSpacesService(OC_ACCOUNT_NAME) + ocSpaceService.getSpacePermissions(OC_SPACE_PROJECT_WITH_IMAGE.id) + } + } + + @Test + fun `editSpace updates a project space correctly`() { + val editSpaceOperationResult = createRemoteOperationResultMock(SPACE_RESPONSE, isSuccess = true) + + every { + ocSpaceService.editSpace( + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + } returns editSpaceOperationResult + + val spaceResult = ocRemoteSpacesDataSource.editSpace( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + assertEquals(SPACE_RESPONSE.toModel(OC_ACCOUNT_NAME), spaceResult) + + verify(exactly = 1) { + clientManager.getSpacesService(OC_ACCOUNT_NAME) + ocSpaceService.editSpace( + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + } + } + } 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 1b4bbdfdf17..96f48cbff60 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 @@ -34,6 +34,7 @@ import com.owncloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE import com.owncloud.android.testutil.OC_USER_QUOTA_LIMITED import com.owncloud.android.testutil.OC_USER_QUOTA_UNLIMITED import com.owncloud.android.testutil.OC_USER_QUOTA_WITHOUT_PERSONAL +import com.owncloud.android.testutil.SPACE_PERMISSIONS import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -246,6 +247,20 @@ class OCSpacesRepositoryTest { } } + @Test + fun `getSpacePermissions returns a list of String with space permissions`() { + every { + remoteSpacesDataSource.getSpacePermissions(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) + } returns SPACE_PERMISSIONS + + val spacePermissions = ocSpacesRepository.getSpacePermissions(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) + assertEquals(SPACE_PERMISSIONS, spacePermissions) + + verify(exactly = 1) { + remoteSpacesDataSource.getSpacePermissions(OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.id) + } + } + @Test fun `getWebDavUrlForSpace returns a String of webdav url`() { every { @@ -302,4 +317,35 @@ class OCSpacesRepositoryTest { } } + @Test + fun `editSpace updates a space correctly`() { + every { + remoteSpacesDataSource.editSpace( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + } returns OC_SPACE_PROJECT_WITH_IMAGE + + ocSpacesRepository.editSpace( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + + verify (exactly = 1) { + remoteSpacesDataSource.editSpace( + accountName = OC_ACCOUNT_NAME, + spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id, + spaceName = OC_SPACE_PROJECT_WITH_IMAGE.name, + spaceSubtitle = OC_SPACE_PROJECT_WITH_IMAGE.description!!, + spaceQuota = OC_SPACE_PROJECT_WITH_IMAGE.quota?.total!! + ) + } + } + } 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 f5150969a88..c8ac160dec5 100644 --- a/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt +++ b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCSpace.kt @@ -302,3 +302,8 @@ val SPACE_RESPONSE = ), special = null, ) + +val SPACE_PERMISSIONS = listOf( + "libre.graph/driveItem/permissions/delete", + "libre.graph/driveItem/permissions/update" +) From 396e67bdda4c9886565a852d575e6a8630a554fb Mon Sep 17 00:00:00 2001 From: joragua Date: Tue, 7 Oct 2025 08:22:46 +0200 Subject: [PATCH 6/9] feat: add release note --- .../presentation/releasenotes/ReleaseNotesViewModel.kt | 5 +++++ owncloudApp/src/main/res/values/strings.xml | 2 ++ 2 files changed, 7 insertions(+) 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 acc58f234f7..31456d6c77b 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 @@ -53,6 +53,11 @@ class ReleaseNotesViewModel( subtitle = R.string.release_notes_4_7_0_subtitle_create_new_spaces, type = ReleaseNoteType.ENHANCEMENT ), + ReleaseNote( + title = R.string.release_notes_4_7_0_title_edit_spaces, + subtitle = R.string.release_notes_4_7_0_subtitle_edit_spaces, + type = ReleaseNoteType.ENHANCEMENT + ), ReleaseNote( title = R.string.release_notes_bugfixes_title, subtitle = R.string.release_notes_bugfixes_subtitle, diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index cd634783ae2..6d662c72a50 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -743,6 +743,8 @@ A new design for Infinite Scale users in the spaces list, with a three-dot button on each space card that opens an options menu when clicked Create project spaces Infinite Scale users with right permissions can now create project spaces directly from the app + Edit project spaces + Infinite Scale users with right permissions can now edit project spaces directly from the app Open in web Open in %1$s (web) From 145347c6b65a3bc68e8668eb18083c6587fe8992 Mon Sep 17 00:00:00 2001 From: joragua Date: Tue, 7 Oct 2025 08:46:23 +0200 Subject: [PATCH 7/9] chore: add calens file --- changelog/unreleased/4687 | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/unreleased/4687 diff --git a/changelog/unreleased/4687 b/changelog/unreleased/4687 new file mode 100644 index 00000000000..894f738b4a1 --- /dev/null +++ b/changelog/unreleased/4687 @@ -0,0 +1,7 @@ +Enhancement: Edit a space + +A new edit space option has been added to the bottom sheet, available only to users +with the required permissions when the three-dot menu button is tapped. + +https://github.com/owncloud/android/issues/4607 +https://github.com/owncloud/android/pull/4687 From 4ca1b8e487cfaa88b1b5c94cd4c476da7618fc15 Mon Sep 17 00:00:00 2001 From: joragua Date: Tue, 7 Oct 2025 17:20:38 +0200 Subject: [PATCH 8/9] refactor: use let block instead of non-null assertion to handle space id --- .../createspace/CreateSpaceDialogFragment.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt index 96340e0bb84..e8eeed30889 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/createspace/CreateSpaceDialogFragment.kt @@ -78,12 +78,14 @@ class CreateSpaceDialogFragment : DialogFragment() { val spaceQuota = convertToBytes(createSpaceDialogQuotaUnit.selectedItem.toString()) if (isEditMode) { - createSpaceListener.editSpace( - spaceId = currentSpace!!.id, - spaceName = spaceName, - spaceSubtitle = spaceSubtitle, - spaceQuota = if (canEditQuota) spaceQuota else null - ) + currentSpace?.let { + createSpaceListener.editSpace( + spaceId = it.id, + spaceName = spaceName, + spaceSubtitle = spaceSubtitle, + spaceQuota = if (canEditQuota) spaceQuota else null + ) + } } else { createSpaceListener.createSpace( spaceName = spaceName, @@ -127,7 +129,7 @@ class CreateSpaceDialogFragment : DialogFragment() { val totalGB = spaceQuota / 1_000_000_000.0 return when (totalGB) { 1.0 -> 0 - 2.0-> 1 + 2.0 -> 1 5.0 -> 2 10.0 -> 3 50.0 -> 4 From 81c0914fd3129cf2cde424b91aef4517697944fc Mon Sep 17 00:00:00 2001 From: ownClouders Date: Wed, 8 Oct 2025 09:21:48 +0000 Subject: [PATCH 9/9] docs: calens changelog updated --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c72c25d5f9..26e487afcd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ ownCloud admins and users. * Enhancement - New layout for spaces list: [#4604](https://github.com/owncloud/android/issues/4604) * Enhancement - Add account ID to the user information: [#4605](https://github.com/owncloud/android/issues/4605) * Enhancement - Create a new space: [#4606](https://github.com/owncloud/android/issues/4606) +* Enhancement - Edit a space: [#4607](https://github.com/owncloud/android/issues/4607) * Enhancement - Update test in GitHub Actions: [#4663](https://github.com/owncloud/android/pull/4663) * Enhancement - New workflow to generate a build from "latest" tag on demand: [#4681](https://github.com/owncloud/android/pull/4681) @@ -120,6 +121,14 @@ ownCloud admins and users. https://github.com/owncloud/android/issues/4606 https://github.com/owncloud/android/pull/4683 +* Enhancement - Edit a space: [#4607](https://github.com/owncloud/android/issues/4607) + + A new edit space option has been added to the bottom sheet, available only to + users with the required permissions when the three-dot menu button is tapped. + + https://github.com/owncloud/android/issues/4607 + https://github.com/owncloud/android/pull/4687 + * Enhancement - Update test in GitHub Actions: [#4663](https://github.com/owncloud/android/pull/4663) A new Github Actions workflow has been added, in order to check whether the