Skip to content

Commit 0daaf2a

Browse files
committed
feat: implement methods and network operation to update a space on the server
1 parent cf2c65e commit 0daaf2a

File tree

15 files changed

+302
-45
lines changed

15 files changed

+302
-45
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUse
9090
import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase
9191
import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase
9292
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
93+
import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase
9394
import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
9495
import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase
9596
import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase
@@ -224,6 +225,7 @@ val useCaseModule = module {
224225

225226
// Spaces
226227
factoryOf(::CreateSpaceUseCase)
228+
factoryOf(::EditSpaceUseCase)
227229
factoryOf(::FilterSpaceMenuOptionsUseCase)
228230
factoryOf(::GetPersonalAndProjectSpacesForAccountUseCase)
229231
factoryOf(::GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase)

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(), get(), accountName, showPersonalSpace)
105+
SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName, showPersonalSpace)
106106
}
107107
}

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

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -204,45 +204,18 @@ class SpacesListFragment :
204204
}
205205
}
206206

207-
collectLatestLifecycleFlow(spacesListViewModel.menuOptions) { menuOptions ->
208-
val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater)
209-
val dialog = BottomSheetDialog(requireContext())
210-
dialog.setContentView(spaceOptionsBottomSheetBinding.root)
211-
212-
val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(
213-
spaceOptionsBottomSheetBinding.root.parent as View)
214-
val closeBottomSheetButton = spaceOptionsBottomSheetBinding.closeBottomSheet
215-
closeBottomSheetButton.setOnClickListener {
216-
dialog.dismiss()
217-
}
218-
219-
val thumbnailBottomSheet = spaceOptionsBottomSheetBinding.thumbnailBottomSheet
220-
thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space)
221-
222-
val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet
223-
spaceNameBottomSheet.text = currentSpace.name
224-
225-
val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet
226-
spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(currentSpace.quota?.used ?: 0L, requireContext(), true)
227-
228-
val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet
229-
spaceSeparatorBottomSheet.visibility = View.GONE
230-
231-
menuOptions.forEach { menuOption ->
232-
setMenuOption(menuOption, spaceOptionsBottomSheetBinding, dialog)
233-
}
234-
235-
fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
236-
override fun onStateChanged(bottomSheet: View, newState: Int) {
237-
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
238-
fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED
239-
}
207+
collectLatestLifecycleFlow(spacesListViewModel.editSpaceFlow) { event ->
208+
event?.let {
209+
when (val uiResult = event.peekContent()) {
210+
is UIResult.Success -> { showMessageInSnackbar(getString(R.string.edit_space_correctly)) }
211+
is UIResult.Loading -> { }
212+
is UIResult.Error -> { showErrorInSnackbar(R.string.edit_space_failed, uiResult.error) }
240213
}
241-
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
242-
})
214+
}
215+
}
243216

244-
dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = spaceOptionsBottomSheetBinding.root.measuredHeight }
245-
dialog.show()
217+
collectLatestLifecycleFlow(spacesListViewModel.menuOptions) { menuOptions ->
218+
showSpaceMenuOptionsDialog(menuOptions)
246219
}
247220

248221
}
@@ -293,6 +266,10 @@ class SpacesListFragment :
293266
spacesListViewModel.createSpace(spaceName, spaceSubtitle, spaceQuota)
294267
}
295268

269+
override fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) {
270+
spacesListViewModel.editSpace(spaceId, spaceName, spaceSubtitle, spaceQuota)
271+
}
272+
296273
fun setSearchListener(searchView: SearchView) {
297274
searchView.setOnQueryTextListener(this)
298275
}
@@ -302,6 +279,47 @@ class SpacesListFragment :
302279
searchViewRootToolbar.queryHint = getString(R.string.actionbar_search_space)
303280
}
304281

282+
private fun showSpaceMenuOptionsDialog(menuOptions: List<SpaceMenuOption>) {
283+
val spaceOptionsBottomSheetBinding = FileOptionsBottomSheetFragmentBinding.inflate(layoutInflater)
284+
val dialog = BottomSheetDialog(requireContext())
285+
dialog.setContentView(spaceOptionsBottomSheetBinding.root)
286+
287+
val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(
288+
spaceOptionsBottomSheetBinding.root.parent as View)
289+
val closeBottomSheetButton = spaceOptionsBottomSheetBinding.closeBottomSheet
290+
closeBottomSheetButton.setOnClickListener {
291+
dialog.dismiss()
292+
}
293+
294+
val thumbnailBottomSheet = spaceOptionsBottomSheetBinding.thumbnailBottomSheet
295+
thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_space)
296+
297+
val spaceNameBottomSheet = spaceOptionsBottomSheetBinding.fileNameBottomSheet
298+
spaceNameBottomSheet.text = currentSpace.name
299+
300+
val spaceSizeBottomSheet = spaceOptionsBottomSheetBinding.fileSizeBottomSheet
301+
spaceSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(currentSpace.quota?.used ?: 0L, requireContext(), true)
302+
303+
val spaceSeparatorBottomSheet = spaceOptionsBottomSheetBinding.fileSeparatorBottomSheet
304+
spaceSeparatorBottomSheet.visibility = View.GONE
305+
306+
menuOptions.forEach { menuOption ->
307+
setMenuOption(menuOption, spaceOptionsBottomSheetBinding, dialog)
308+
}
309+
310+
fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
311+
override fun onStateChanged(bottomSheet: View, newState: Int) {
312+
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
313+
fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED
314+
}
315+
}
316+
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
317+
})
318+
319+
dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = spaceOptionsBottomSheetBinding.root.measuredHeight }
320+
dialog.show()
321+
}
322+
305323
private fun setMenuOption(menuOption: SpaceMenuOption, binding: FileOptionsBottomSheetFragmentBinding, dialog: BottomSheetDialog) {
306324
val fileOptionItemView = BottomSheetFragmentItemView(requireContext())
307325
fileOptionItemView.apply {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase
3131
import com.owncloud.android.domain.spaces.model.OCSpace
3232
import com.owncloud.android.domain.spaces.model.SpaceMenuOption
3333
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
34+
import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase
3435
import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
3536
import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase
3637
import com.owncloud.android.domain.spaces.usecases.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase
@@ -60,6 +61,7 @@ class SpacesListViewModel(
6061
private val getUserPermissionsAsyncUseCase: GetUserPermissionsAsyncUseCase,
6162
private val createSpaceUseCase: CreateSpaceUseCase,
6263
private val filterSpaceMenuOptionsUseCase: FilterSpaceMenuOptionsUseCase,
64+
private val editSpaceUseCase: EditSpaceUseCase,
6365
private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider,
6466
private val accountName: String,
6567
private val showPersonalSpace: Boolean,
@@ -81,6 +83,9 @@ class SpacesListViewModel(
8183
private val _createSpaceFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
8284
val createSpaceFlow: SharedFlow<Event<UIResult<Unit>>?> = _createSpaceFlow
8385

86+
private val _editSpaceFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
87+
val editSpaceFlow: SharedFlow<Event<UIResult<Unit>>?> = _editSpaceFlow
88+
8489
init {
8590
viewModelScope.launch(coroutinesDispatcherProvider.io) {
8691
refreshSpacesFromServer()
@@ -161,6 +166,16 @@ class SpacesListViewModel(
161166
}
162167
}
163168

169+
fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?) {
170+
viewModelScope.launch(coroutinesDispatcherProvider.io) {
171+
when (val result = editSpaceUseCase(EditSpaceUseCase.Params(accountName, spaceId, spaceName, spaceSubtitle, spaceQuota))) {
172+
is UseCaseResult.Success -> _editSpaceFlow.emit(Event(UIResult.Success(result.getDataOrNull())))
173+
is UseCaseResult.Error -> _editSpaceFlow.emit(Event(UIResult.Error(error = result.getThrowableOrNull())))
174+
}
175+
refreshSpacesFromServerAsyncUseCase(RefreshSpacesFromServerAsyncUseCase.Params(accountName))
176+
}
177+
}
178+
164179
fun filterMenuOptions(space: OCSpace, editSpacesPermission: Boolean) {
165180
viewModelScope.launch(coroutinesDispatcherProvider.io) {
166181
val result = filterSpaceMenuOptionsUseCase(

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class CreateSpaceDialogFragment : DialogFragment() {
4848
super.onViewCreated(view, savedInstanceState)
4949
val currentSpace = requireArguments().getParcelable<OCSpace>(ARG_CURRENT_SPACE)
5050
val isEditMode = requireArguments().getBoolean(ARG_EDIT_SPACE_MODE)
51+
val canEditQuota = requireArguments().getBoolean(ARG_CAN_EDIT_SPACE_QUOTA)
5152
binding.apply {
5253
cancelCreateSpaceButton.setOnClickListener { dialog?.dismiss() }
5354
createSpaceDialogNameValue.doOnTextChanged { name, _, _, _ ->
@@ -57,7 +58,6 @@ class CreateSpaceDialogFragment : DialogFragment() {
5758

5859
if (isEditMode) {
5960
createSpaceDialogTitle.text = getString(R.string.edit_space)
60-
val canEditQuota = requireArguments().getBoolean(ARG_CAN_EDIT_SPACE_QUOTA)
6161
createSpaceDialogQuotaSection.isVisible = canEditQuota
6262

6363
currentSpace?.let {
@@ -72,12 +72,24 @@ class CreateSpaceDialogFragment : DialogFragment() {
7272
}
7373

7474
createSpaceButton.setOnClickListener {
75-
val spaceQuota = convertToBytes(binding.createSpaceDialogQuotaUnit.selectedItem.toString())
76-
createSpaceListener.createSpace(
77-
spaceName = binding.createSpaceDialogNameValue.text.toString(),
78-
spaceSubtitle = binding.createSpaceDialogSubtitleValue.text.toString(),
79-
spaceQuota = spaceQuota
80-
)
75+
val spaceName = createSpaceDialogNameValue.text.toString()
76+
val spaceSubtitle = createSpaceDialogSubtitleValue.text.toString()
77+
val spaceQuota = convertToBytes(createSpaceDialogQuotaUnit.selectedItem.toString())
78+
79+
if (isEditMode) {
80+
createSpaceListener.editSpace(
81+
spaceId = currentSpace!!.id,
82+
spaceName = spaceName,
83+
spaceSubtitle = spaceSubtitle,
84+
spaceQuota = if (canEditQuota) spaceQuota else null
85+
)
86+
} else {
87+
createSpaceListener.createSpace(
88+
spaceName = spaceName,
89+
spaceSubtitle = spaceSubtitle,
90+
spaceQuota = spaceQuota
91+
)
92+
}
8193
dialog?.dismiss()
8294
}
8395
}
@@ -112,6 +124,7 @@ class CreateSpaceDialogFragment : DialogFragment() {
112124

113125
interface CreateSpaceListener {
114126
fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long)
127+
fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?)
115128
}
116129

117130
companion object {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,8 @@
852852
<string name="create_space_failed">Space could not be created</string>
853853
<string name="edit_space">Edit space</string>
854854
<string name="edit_space_button">Edit</string>
855+
<string name="edit_space_correctly">Space updated correctly</string>
856+
<string name="edit_space_failed">Space could not be updated</string>
855857

856858
<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>
857859

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* ownCloud Android Library is available under MIT license
2+
* Copyright (C) 2025 ownCloud GmbH.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18+
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19+
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
* THE SOFTWARE.
22+
*
23+
*/
24+
25+
package com.owncloud.android.lib.common.http.methods.nonwebdav
26+
27+
import okhttp3.OkHttpClient
28+
import okhttp3.RequestBody
29+
import java.io.IOException
30+
import java.net.URL
31+
32+
/**
33+
* OkHttp patch calls wrapper
34+
*
35+
* @author Jorge Aguado Recio
36+
*/
37+
class PatchMethod(
38+
url: URL,
39+
private val patchRequestBody: RequestBody
40+
) : HttpMethod(url) {
41+
@Throws(IOException::class)
42+
override fun onExecute(okHttpClient: OkHttpClient): Int {
43+
request = request.newBuilder()
44+
.patch(patchRequestBody)
45+
.build()
46+
return super.onExecute(okHttpClient)
47+
}
48+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
22+
package com.owncloud.android.lib.resources.spaces
23+
24+
import com.owncloud.android.lib.common.OwnCloudClient
25+
import com.owncloud.android.lib.common.http.HttpConstants
26+
import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_JSON
27+
import com.owncloud.android.lib.common.http.methods.nonwebdav.PatchMethod
28+
import com.owncloud.android.lib.common.operations.RemoteOperation
29+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
30+
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
31+
import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse
32+
import com.squareup.moshi.JsonAdapter
33+
import com.squareup.moshi.Moshi
34+
import okhttp3.MediaType.Companion.toMediaType
35+
import okhttp3.RequestBody.Companion.toRequestBody
36+
import org.json.JSONObject
37+
import timber.log.Timber
38+
import java.net.URL
39+
40+
class EditRemoteSpaceOperation(
41+
private val spaceId: String,
42+
private val spaceName: String,
43+
private val spaceSubtitle: String,
44+
private val spaceQuota: Long?
45+
): RemoteOperation<SpaceResponse>() {
46+
override fun run(client: OwnCloudClient): RemoteOperationResult<SpaceResponse> {
47+
var result: RemoteOperationResult<SpaceResponse>
48+
try {
49+
val moshi = Moshi.Builder().build()
50+
51+
val uriBuilder = client.baseUri.buildUpon().apply {
52+
appendEncodedPath(GRAPH_API_SPACES_PATH)
53+
appendEncodedPath(spaceId)
54+
}
55+
56+
val requestBody = JSONObject().apply {
57+
put(SPACE_NAME_BODY_PARAM, spaceName)
58+
spaceQuota?.let {
59+
put(SPACE_QUOTA_BODY_PARAM, JSONObject().apply {
60+
put(SPACE_QUOTA_TOTAL_BODY_PARAM, spaceQuota)
61+
})
62+
}
63+
put(SPACE_DESCRIPTION_BODY_PARAM, spaceSubtitle)
64+
}.toString().toRequestBody(CONTENT_TYPE_JSON.toMediaType())
65+
66+
val patchMethod = PatchMethod(URL(uriBuilder.build().toString()), requestBody)
67+
68+
val status = client.executeHttpMethod(patchMethod)
69+
70+
val response = patchMethod.getResponseBodyAsString()
71+
72+
if (status == HttpConstants.HTTP_OK) {
73+
Timber.d("Successful response: $response")
74+
75+
val responseAdapter: JsonAdapter<SpaceResponse> = moshi.adapter(SpaceResponse::class.java)
76+
77+
result = RemoteOperationResult(ResultCode.OK)
78+
result.data = responseAdapter.fromJson(response)
79+
80+
Timber.d("Update of space completed and parsed to ${result.data}")
81+
} else {
82+
result = RemoteOperationResult(patchMethod)
83+
Timber.e("Failed response while updating the space; status code: $status, response: $response")
84+
}
85+
} catch (e: Exception) {
86+
result = RemoteOperationResult(e)
87+
Timber.e(e, "Exception while updating the space $spaceId")
88+
}
89+
return result
90+
}
91+
92+
companion object {
93+
private const val GRAPH_API_SPACES_PATH = "graph/v1.0/drives/"
94+
private const val SPACE_NAME_BODY_PARAM = "name"
95+
private const val SPACE_QUOTA_BODY_PARAM = "quota"
96+
private const val SPACE_QUOTA_TOTAL_BODY_PARAM = "total"
97+
private const val SPACE_DESCRIPTION_BODY_PARAM = "description"
98+
}
99+
100+
}

0 commit comments

Comments
 (0)