Skip to content

Commit 7c93afd

Browse files
committed
feat: add space edit option in bottom sheet dialog for users with proper permissions
1 parent eaf5788 commit 7c93afd

File tree

16 files changed

+351
-37
lines changed

16 files changed

+351
-37
lines changed

owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,18 @@ import com.owncloud.android.domain.sharing.shares.usecases.EditPublicShareAsyncU
8989
import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUseCase
9090
import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase
9191
import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase
92+
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
93+
import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
9294
import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase
9395
import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase
9496
import com.owncloud.android.domain.spaces.usecases.GetPersonalSpaceForAccountUseCase
9597
import com.owncloud.android.domain.spaces.usecases.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase
9698
import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase
9799
import com.owncloud.android.domain.spaces.usecases.GetSpaceByIdForAccountUseCase
100+
import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase
98101
import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase
99102
import com.owncloud.android.domain.spaces.usecases.GetSpacesFromEveryAccountUseCaseAsStream
100103
import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase
101-
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
102104
import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase
103105
import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsStreamUseCase
104106
import com.owncloud.android.domain.transfers.usecases.GetAllTransfersUseCase
@@ -222,12 +224,14 @@ val useCaseModule = module {
222224

223225
// Spaces
224226
factoryOf(::CreateSpaceUseCase)
227+
factoryOf(::FilterSpaceMenuOptionsUseCase)
225228
factoryOf(::GetPersonalAndProjectSpacesForAccountUseCase)
226229
factoryOf(::GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase)
227230
factoryOf(::GetPersonalSpaceForAccountUseCase)
228231
factoryOf(::GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase)
229232
factoryOf(::GetProjectSpacesWithSpecialsForAccountAsStreamUseCase)
230233
factoryOf(::GetSpaceByIdForAccountUseCase)
234+
factoryOf(::GetSpacePermissionsAsyncUseCase)
231235
factoryOf(::GetSpaceWithSpecialsByIdForAccountUseCase)
232236
factoryOf(::GetSpacesFromEveryAccountUseCaseAsStream)
233237
factoryOf(::GetWebDavUrlForSpaceUseCase)

owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,6 @@ val viewModelModule = module {
102102
get()) }
103103
viewModel { ReceiveExternalFilesViewModel(get(), get(), get(), get()) }
104104
viewModel { (accountName: String, showPersonalSpace: Boolean) ->
105-
SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace)
105+
SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace)
106106
}
107107
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* ownCloud Android client application
3+
*
4+
* @author Jorge Aguado Recio
5+
*
6+
* Copyright (C) 2025 ownCloud GmbH.
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License version 2,
10+
* as published by the Free Software Foundation.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.owncloud.android.extensions
22+
23+
import com.owncloud.android.R
24+
import com.owncloud.android.domain.spaces.model.SpaceMenuOption
25+
26+
fun SpaceMenuOption.toStringResId() =
27+
when (this) {
28+
SpaceMenuOption.EDIT -> R.string.edit_space
29+
}
30+
31+
fun SpaceMenuOption.toDrawableResId() =
32+
when (this) {
33+
SpaceMenuOption.EDIT -> R.drawable.ic_pencil
34+
}

owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import android.view.MenuInflater
3030
import android.view.View
3131
import android.view.ViewGroup
3232
import androidx.appcompat.widget.SearchView
33+
import androidx.core.content.res.ResourcesCompat
3334
import androidx.core.os.bundleOf
3435
import androidx.core.view.isVisible
3536
import androidx.fragment.app.Fragment
@@ -42,13 +43,17 @@ import com.owncloud.android.databinding.FileOptionsBottomSheetFragmentBinding
4243
import com.owncloud.android.databinding.SpacesListFragmentBinding
4344
import com.owncloud.android.domain.files.model.FileListOption
4445
import com.owncloud.android.domain.spaces.model.OCSpace
46+
import com.owncloud.android.domain.spaces.model.SpaceMenuOption
4547
import com.owncloud.android.extensions.collectLatestLifecycleFlow
4648
import com.owncloud.android.extensions.showErrorInSnackbar
4749
import com.owncloud.android.extensions.showMessageInSnackbar
4850
import com.owncloud.android.extensions.toDrawableRes
51+
import com.owncloud.android.extensions.toDrawableResId
52+
import com.owncloud.android.extensions.toStringResId
4953
import com.owncloud.android.extensions.toSubtitleStringRes
5054
import com.owncloud.android.extensions.toTitleStringRes
5155
import com.owncloud.android.presentation.capabilities.CapabilityViewModel
56+
import com.owncloud.android.presentation.common.BottomSheetFragmentItemView
5257
import com.owncloud.android.utils.DisplayUtils
5358
import com.owncloud.android.presentation.common.UIResult
5459
import com.owncloud.android.presentation.spaces.createspace.CreateSpaceDialogFragment
@@ -66,6 +71,8 @@ class SpacesListFragment :
6671
private val binding get() = _binding!!
6772

6873
private var isMultiPersonal = false
74+
private var editSpacesPermission: Boolean = false
75+
private lateinit var currentSpace: OCSpace
6976

7077
private val spacesListViewModel: SpacesListViewModel by viewModel {
7178
parametersOf(
@@ -166,7 +173,10 @@ class SpacesListFragment :
166173
when (val uiResult = event.peekContent()) {
167174
is UIResult.Success -> {
168175
Timber.d("The permissions for $accountName are: ${uiResult.data}")
169-
uiResult.data?.let { binding.fabCreateSpace.isVisible = it.contains(DRIVES_CREATE_ALL_PERMISSION) }
176+
uiResult.data?.let {
177+
binding.fabCreateSpace.isVisible = it.contains(DRIVES_CREATE_ALL_PERMISSION)
178+
editSpacesPermission = it.contains(DRIVES_READ_WRITE_ALL_PERMISSION)
179+
}
170180
}
171181
is UIResult.Loading -> { }
172182
is UIResult.Error -> {
@@ -187,6 +197,47 @@ class SpacesListFragment :
187197
}
188198
}
189199

200+
collectLatestLifecycleFlow(spacesListViewModel.menuOptions) { menuOptions ->
201+
val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater)
202+
val dialog = BottomSheetDialog(requireContext())
203+
dialog.setContentView(spaceOptionsBottomSheetBinding.root)
204+
205+
val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(
206+
spaceOptionsBottomSheetBinding.root.parent as View)
207+
val closeBottomSheetButton = spaceOptionsBottomSheetBinding.closeBottomSheet
208+
closeBottomSheetButton.setOnClickListener {
209+
dialog.dismiss()
210+
}
211+
212+
val thumbnailBottomSheet = spaceOptionsBottomSheetBinding.thumbnailBottomSheet
213+
thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space)
214+
215+
val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet
216+
spaceNameBottomSheet.text = currentSpace.name
217+
218+
val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet
219+
spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(currentSpace.quota?.used ?: 0L, requireContext(), true)
220+
221+
val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet
222+
spaceSeparatorBottomSheet.visibility = View.GONE
223+
224+
menuOptions.forEach { menuOption ->
225+
setMenuOption(menuOption, spaceOptionsBottomSheetBinding, dialog)
226+
}
227+
228+
fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
229+
override fun onStateChanged(bottomSheet: View, newState: Int) {
230+
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
231+
fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED
232+
}
233+
}
234+
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
235+
})
236+
237+
dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = spaceOptionsBottomSheetBinding.root.measuredHeight }
238+
dialog.show()
239+
}
240+
190241
}
191242

192243
private fun showOrHideEmptyView(spacesList: List<OCSpace>) {
@@ -211,40 +262,8 @@ class SpacesListFragment :
211262
}
212263

213264
override fun onThreeDotButtonClick(ocSpace: OCSpace) {
214-
val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater)
215-
val dialog = BottomSheetDialog(requireContext())
216-
dialog.setContentView(spaceOptionsBottomSheetBinding.root)
217-
218-
val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(
219-
spaceOptionsBottomSheetBinding.root.parent as View)
220-
val closeBottomSheetButton = spaceOptionsBottomSheetBinding.closeBottomSheet
221-
closeBottomSheetButton.setOnClickListener {
222-
dialog.dismiss()
223-
}
224-
225-
val thumbnailBottomSheet = spaceOptionsBottomSheetBinding.thumbnailBottomSheet
226-
thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space)
227-
228-
val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet
229-
spaceNameBottomSheet.text = ocSpace.name
230-
231-
val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet
232-
spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(ocSpace.quota?.used ?: 0L, requireContext(), true)
233-
234-
val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet
235-
spaceSeparatorBottomSheet.visibility = View.GONE
236-
237-
fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
238-
override fun onStateChanged(bottomSheet: View, newState: Int) {
239-
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
240-
fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED
241-
}
242-
}
243-
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
244-
})
245-
246-
dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = spaceOptionsBottomSheetBinding.root.measuredHeight }
247-
dialog.show()
265+
currentSpace = ocSpace
266+
spacesListViewModel.filterMenuOptions(ocSpace, editSpacesPermission)
248267
}
249268

250269
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -276,12 +295,32 @@ class SpacesListFragment :
276295
searchViewRootToolbar.queryHint = getString(R.string.actionbar_search_space)
277296
}
278297

298+
private fun setMenuOption(menuOption: SpaceMenuOption, binding: FileOptionsBottomSheetFragmentBinding, dialog: BottomSheetDialog) {
299+
val fileOptionItemView = BottomSheetFragmentItemView(requireContext())
300+
fileOptionItemView.apply {
301+
itemIcon = ResourcesCompat.getDrawable(resources, menuOption.toDrawableResId(), null)
302+
title = getString(menuOption.toStringResId())
303+
setOnClickListener {
304+
dialog.dismiss()
305+
when(menuOption) {
306+
SpaceMenuOption.EDIT -> {
307+
val accountName = requireArguments().getString(BUNDLE_ACCOUNT_NAME)
308+
val editDialog = CreateSpaceDialogFragment.newInstance(accountName, this@SpacesListFragment)
309+
editDialog.show(requireActivity().supportFragmentManager, DIALOG_CREATE_SPACE)
310+
}
311+
}
312+
}
313+
}
314+
binding.fileOptionsBottomSheetLayout.addView(fileOptionItemView)
315+
}
316+
279317
companion object {
280318
const val REQUEST_KEY_CLICK_SPACE = "REQUEST_KEY_CLICK_SPACE"
281319
const val BUNDLE_KEY_CLICK_SPACE = "BUNDLE_KEY_CLICK_SPACE"
282320
const val BUNDLE_SHOW_PERSONAL_SPACE = "showPersonalSpace"
283321
const val BUNDLE_ACCOUNT_NAME = "accountName"
284322
const val DRIVES_CREATE_ALL_PERMISSION = "Drives.Create.all"
323+
const val DRIVES_READ_WRITE_ALL_PERMISSION = "Drives.ReadWrite.all"
285324

286325
private const val DIALOG_CREATE_SPACE = "DIALOG_CREATE_SPACE"
287326

owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import com.owncloud.android.domain.files.model.OCFile
2929
import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PATH
3030
import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase
3131
import com.owncloud.android.domain.spaces.model.OCSpace
32+
import com.owncloud.android.domain.spaces.model.SpaceMenuOption
3233
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
34+
import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
3335
import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase
3436
import com.owncloud.android.domain.spaces.usecases.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase
3537
import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase
@@ -57,6 +59,7 @@ class SpacesListViewModel(
5759
private val getUserIdAsyncUseCase: GetUserIdAsyncUseCase,
5860
private val getUserPermissionsAsyncUseCase: GetUserPermissionsAsyncUseCase,
5961
private val createSpaceUseCase: CreateSpaceUseCase,
62+
private val filterSpaceMenuOptionsUseCase: FilterSpaceMenuOptionsUseCase,
6063
private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider,
6164
private val accountName: String,
6265
private val showPersonalSpace: Boolean,
@@ -72,6 +75,9 @@ class SpacesListViewModel(
7275
private val _userPermissions = MutableStateFlow<Event<UIResult<List<String>>>?>(null)
7376
val userPermissions: StateFlow<Event<UIResult<List<String>>>?> = _userPermissions
7477

78+
private val _menuOptions: MutableSharedFlow<List<SpaceMenuOption>> = MutableSharedFlow()
79+
val menuOptions: SharedFlow<List<SpaceMenuOption>> = _menuOptions
80+
7581
private val _createSpaceFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
7682
val createSpaceFlow: SharedFlow<Event<UIResult<Unit>>?> = _createSpaceFlow
7783

@@ -155,6 +161,19 @@ class SpacesListViewModel(
155161
}
156162
}
157163

164+
fun filterMenuOptions(space: OCSpace, editSpacesPermission: Boolean) {
165+
viewModelScope.launch(coroutinesDispatcherProvider.io) {
166+
val result = filterSpaceMenuOptionsUseCase(
167+
FilterSpaceMenuOptionsUseCase.Params(
168+
accountName = accountName,
169+
space = space,
170+
editSpacesPermission = editSpacesPermission
171+
)
172+
)
173+
_menuOptions.emit(result)
174+
}
175+
}
176+
158177
data class SpacesListUiState(
159178
val spaces: List<OCSpace>,
160179
val rootFolderFromSelectedSpace: OCFile? = null,

owncloudApp/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,7 @@
850850
<string name="create_space_dialog_characters_error">Forbidden characters: / \\ . : ? * &quot; ' &gt; &lt; |</string>
851851
<string name="create_space_correctly">Space created correctly</string>
852852
<string name="create_space_failed">Space could not be created</string>
853+
<string name="edit_space">Edit space</string>
853854

854855
<string name="feedback_dialog_get_in_contact_description"><![CDATA[ Ask for help in our <a href=\"%1$s\"><b>forum</b></a> or contribute in our <a href=\"%2$s\"><b>GitHub repo</b></a>]]></string>
855856

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* ownCloud Android client application
3+
*
4+
* @author Jorge Aguado Recio
5+
*
6+
* Copyright (C) 2025 ownCloud GmbH.
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License version 2,
10+
* as published by the Free Software Foundation.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.owncloud.android.lib.resources.spaces
22+
23+
import com.owncloud.android.lib.common.OwnCloudClient
24+
import com.owncloud.android.lib.common.http.HttpConstants
25+
import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod
26+
import com.owncloud.android.lib.common.operations.RemoteOperation
27+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
28+
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
29+
import com.squareup.moshi.Json
30+
import com.squareup.moshi.JsonAdapter
31+
import com.squareup.moshi.JsonClass
32+
import com.squareup.moshi.Moshi
33+
import timber.log.Timber
34+
import java.net.URL
35+
36+
class GetRemoteSpacePermissionsOperation(
37+
private val spaceId: String
38+
): RemoteOperation<List<String>>() {
39+
override fun run(client: OwnCloudClient): RemoteOperationResult<List<String>> {
40+
var result: RemoteOperationResult<List<String>>
41+
try {
42+
val requestUri = client.baseUri.buildUpon().apply {
43+
appendEncodedPath(GRAPH_API_SPACES_PATH)
44+
appendEncodedPath(spaceId)
45+
appendEncodedPath(SPACE_PERMISSIONS_ENDPOINT)
46+
build()
47+
}
48+
val getMethod = GetMethod(URL(requestUri.toString()))
49+
50+
val status = client.executeHttpMethod(getMethod)
51+
52+
val response = getMethod.getResponseBodyAsString()
53+
54+
if (status == HttpConstants.HTTP_OK) {
55+
Timber.d("Successful response: $response")
56+
57+
val moshi: Moshi = Moshi.Builder().build()
58+
val adapter: JsonAdapter<SpacePermissionsListResponse> = moshi.adapter(SpacePermissionsListResponse::class.java)
59+
60+
result = RemoteOperationResult(ResultCode.OK)
61+
result.data = getMethod.getResponseBodyAsString().let { adapter.fromJson(it)?.permissions ?: emptyList() }
62+
63+
Timber.d("Get space permissions for user completed and parsed to ${result.data}")
64+
} else {
65+
result = RemoteOperationResult(getMethod)
66+
Timber.e("Failed response while getting space permissions; status code: $status, response: $response")
67+
}
68+
} catch (e: Exception) {
69+
result = RemoteOperationResult(e)
70+
Timber.e(e, "Exception while getting space permissions for user")
71+
}
72+
return result
73+
}
74+
75+
@JsonClass(generateAdapter = true)
76+
data class SpacePermissionsListResponse(
77+
@Json(name = "@libre.graph.permissions.actions.allowedValues")
78+
val permissions: List<String>
79+
)
80+
81+
companion object {
82+
private const val GRAPH_API_SPACES_PATH = "graph/v1beta1/drives/"
83+
private const val SPACE_PERMISSIONS_ENDPOINT = "root/permissions"
84+
}
85+
}

0 commit comments

Comments
 (0)