Skip to content

Commit f6f9413

Browse files
authored
Merge pull request #4705 from owncloud/feature/update_space_image
[FEATURE REQUEST] Update space image
2 parents 7ec0e78 + 9a8f51b commit f6f9413

File tree

21 files changed

+333
-3
lines changed

21 files changed

+333
-3
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ ownCloud admins and users.
4848
* Enhancement - Create a new space: [#4606](https://github.com/owncloud/android/issues/4606)
4949
* Enhancement - Edit a space: [#4607](https://github.com/owncloud/android/issues/4607)
5050
* Enhancement - Disable/Remove a space: [#4611](https://github.com/owncloud/android/issues/4611)
51+
* Enhancement - Update space image: [#4691](https://github.com/owncloud/android/issues/4691)
5152
* Enhancement - Show space quota: [#4693](https://github.com/owncloud/android/issues/4693)
5253
* Enhancement - Add user role to spaces: [#4698](https://github.com/owncloud/android/pull/4698)
5354

@@ -134,6 +135,15 @@ ownCloud admins and users.
134135
https://github.com/owncloud/android/issues/4611
135136
https://github.com/owncloud/android/pull/4696
136137

138+
* Enhancement - Update space image: [#4691](https://github.com/owncloud/android/issues/4691)
139+
140+
A new option to update the space image has been added to the bottom sheet,
141+
available only to users with the required permissions when the three-dot menu
142+
button is tapped.
143+
144+
https://github.com/owncloud/android/issues/4691
145+
https://github.com/owncloud/android/pull/4705
146+
137147
* Enhancement - Show space quota: [#4693](https://github.com/owncloud/android/issues/4693)
138148

139149
The used and total values of the space quota have been added to the bottom sheet

changelog/unreleased/4705

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Enhancement: Update space image
2+
3+
A new option to update the space image has been added to the bottom sheet, available
4+
only to users with the required permissions when the three-dot menu button is tapped.
5+
6+
https://github.com/owncloud/android/issues/4691
7+
https://github.com/owncloud/android/pull/4705

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUs
9191
import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase
9292
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
9393
import com.owncloud.android.domain.spaces.usecases.DisableSpaceUseCase
94+
import com.owncloud.android.domain.spaces.usecases.EditSpaceImageUseCase
9495
import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase
9596
import com.owncloud.android.domain.spaces.usecases.EnableSpaceUseCase
9697
import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
@@ -229,6 +230,7 @@ val useCaseModule = module {
229230
// Spaces
230231
factoryOf(::CreateSpaceUseCase)
231232
factoryOf(::DisableSpaceUseCase)
233+
factoryOf(::EditSpaceImageUseCase)
232234
factoryOf(::EditSpaceUseCase)
233235
factoryOf(::EnableSpaceUseCase)
234236
factoryOf(::FilterSpaceMenuOptionsUseCase)

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

owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.owncloud.android.domain.spaces.model.SpaceMenuOption
2626
fun SpaceMenuOption.toStringResId() =
2727
when (this) {
2828
SpaceMenuOption.EDIT -> R.string.edit_space
29+
SpaceMenuOption.EDIT_IMAGE -> R.string.edit_space_image
2930
SpaceMenuOption.DISABLE -> R.string.disable_space
3031
SpaceMenuOption.ENABLE -> R.string.enable_space
3132
SpaceMenuOption.DELETE -> R.string.delete_space
@@ -34,6 +35,7 @@ fun SpaceMenuOption.toStringResId() =
3435
fun SpaceMenuOption.toDrawableResId() =
3536
when (this) {
3637
SpaceMenuOption.EDIT -> R.drawable.ic_pencil
38+
SpaceMenuOption.EDIT_IMAGE -> R.drawable.file_image
3739
SpaceMenuOption.DISABLE -> R.drawable.ic_disable_space
3840
SpaceMenuOption.ENABLE -> R.drawable.ic_enable_space
3941
SpaceMenuOption.DELETE -> R.drawable.ic_action_delete_white

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,22 @@
2222

2323
package com.owncloud.android.presentation.spaces
2424

25+
import android.app.Activity
2526
import android.content.DialogInterface
27+
import android.content.Intent
2628
import android.content.res.Configuration
2729
import android.os.Bundle
2830
import android.view.LayoutInflater
2931
import android.view.Menu
3032
import android.view.MenuInflater
3133
import android.view.View
3234
import android.view.ViewGroup
35+
import androidx.activity.result.contract.ActivityResultContracts
3336
import androidx.appcompat.widget.SearchView
3437
import androidx.core.content.res.ResourcesCompat
3538
import androidx.core.os.bundleOf
3639
import androidx.core.view.isVisible
40+
import androidx.documentfile.provider.DocumentFile
3741
import androidx.fragment.app.Fragment
3842
import androidx.fragment.app.setFragmentResult
3943
import androidx.recyclerview.widget.GridLayoutManager
@@ -43,8 +47,15 @@ import com.owncloud.android.R
4347
import com.owncloud.android.databinding.FileOptionsBottomSheetFragmentBinding
4448
import com.owncloud.android.databinding.SpacesListFragmentBinding
4549
import com.owncloud.android.domain.files.model.FileListOption
50+
import com.owncloud.android.domain.files.model.MIME_BMP
51+
import com.owncloud.android.domain.files.model.MIME_GIF
52+
import com.owncloud.android.domain.files.model.MIME_JPEG
53+
import com.owncloud.android.domain.files.model.MIME_PNG
54+
import com.owncloud.android.domain.files.model.MIME_PREFIX_IMAGE
55+
import com.owncloud.android.domain.files.model.MIME_X_MS_BMP
4656
import com.owncloud.android.domain.spaces.model.OCSpace
4757
import com.owncloud.android.domain.spaces.model.SpaceMenuOption
58+
import com.owncloud.android.domain.transfers.model.TransferStatus
4859
import com.owncloud.android.domain.user.model.UserPermissions
4960
import com.owncloud.android.domain.utils.Event
5061
import com.owncloud.android.extensions.collectLatestLifecycleFlow
@@ -61,6 +72,7 @@ import com.owncloud.android.presentation.common.BottomSheetFragmentItemView
6172
import com.owncloud.android.utils.DisplayUtils
6273
import com.owncloud.android.presentation.common.UIResult
6374
import com.owncloud.android.presentation.spaces.createspace.CreateSpaceDialogFragment
75+
import com.owncloud.android.presentation.transfers.TransfersViewModel
6476
import kotlinx.coroutines.flow.SharedFlow
6577
import org.koin.androidx.viewmodel.ext.android.viewModel
6678
import org.koin.core.parameter.parametersOf
@@ -78,6 +90,8 @@ class SpacesListFragment :
7890
private var isMultiPersonal = false
7991
private var userPermissions = mutableSetOf<UserPermissions>()
8092
private var editQuotaPermission = false
93+
private var lastUpdatedRemotePath: String? = null
94+
private var selectedImageName: String? = null
8195
private lateinit var currentSpace: OCSpace
8296

8397
private val spacesListViewModel: SpacesListViewModel by viewModel {
@@ -91,6 +105,24 @@ class SpacesListFragment :
91105
requireArguments().getString(BUNDLE_ACCOUNT_NAME),
92106
)
93107
}
108+
private val transfersViewModel: TransfersViewModel by viewModel()
109+
110+
private val editSpaceImageLauncher =
111+
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
112+
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
113+
114+
val selectedImageUri = result.data?.data ?: return@registerForActivityResult
115+
val accountName = requireArguments().getString(BUNDLE_ACCOUNT_NAME) ?: return@registerForActivityResult
116+
val documentFile = DocumentFile.fromSingleUri(requireContext(), selectedImageUri) ?: return@registerForActivityResult
117+
selectedImageName = documentFile.name
118+
119+
transfersViewModel.uploadFilesFromContentUri(
120+
accountName = accountName,
121+
listOfContentUris = listOf(selectedImageUri),
122+
uploadFolderPath = SPACE_CONFIG_DIR,
123+
spaceId = currentSpace.id
124+
)
125+
}
94126

95127
private lateinit var spacesListAdapter: SpacesListAdapter
96128

@@ -202,6 +234,7 @@ class SpacesListFragment :
202234

203235
collectSpaceOperationsFlow(spacesListViewModel.createSpaceFlow, R.string.create_space_correctly, R.string.create_space_failed)
204236
collectSpaceOperationsFlow(spacesListViewModel.editSpaceFlow, R.string.edit_space_correctly, R.string.edit_space_failed)
237+
collectSpaceOperationsFlow(spacesListViewModel.editSpaceImageFlow, R.string.edit_space_image_correctly, R.string.edit_space_image_failed)
205238
collectSpaceOperationsFlow(spacesListViewModel.disableSpaceFlow, R.string.disable_space_correctly, R.string.disable_space_failed)
206239
collectSpaceOperationsFlow(spacesListViewModel.enableSpaceFlow, R.string.enable_space_correctly, R.string.enable_space_failed)
207240
collectSpaceOperationsFlow(spacesListViewModel.deleteSpaceFlow, R.string.delete_space_correctly, R.string.delete_space_failed)
@@ -210,6 +243,24 @@ class SpacesListFragment :
210243
showSpaceMenuOptionsDialog(menuOptions)
211244
}
212245

246+
collectLatestLifecycleFlow(transfersViewModel.transfersWithSpaceStateFlow) { transfersWithSpace ->
247+
val remotePath = SPACE_CONFIG_DIR + selectedImageName
248+
val matchedTransfer = transfersWithSpace.map { it.first }.find { it.remotePath == remotePath }
249+
250+
if (matchedTransfer != null && lastUpdatedRemotePath != matchedTransfer.remotePath) {
251+
when(matchedTransfer.status) {
252+
TransferStatus.TRANSFER_SUCCEEDED -> {
253+
spacesListViewModel.editSpaceImage(currentSpace.id, matchedTransfer.remotePath)
254+
lastUpdatedRemotePath = matchedTransfer.remotePath
255+
}
256+
TransferStatus.TRANSFER_FAILED -> {
257+
showMessageInSnackbar(getString(R.string.edit_space_image_failed))
258+
}
259+
else -> { }
260+
}
261+
}
262+
}
263+
213264
}
214265

215266
private fun collectSpaceOperationsFlow(flow: SharedFlow<Event<UIResult<Unit>>?>, successMessage: Int, errorMessage: Int) {
@@ -382,6 +433,14 @@ class SpacesListFragment :
382433
negativeButtonText = getString(R.string.common_no)
383434
)
384435
}
436+
SpaceMenuOption.EDIT_IMAGE -> {
437+
val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
438+
addCategory(Intent.CATEGORY_OPENABLE)
439+
type = MIME_PREFIX_IMAGE
440+
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(MIME_JPEG, MIME_PNG, MIME_BMP, MIME_X_MS_BMP, MIME_GIF))
441+
}
442+
editSpaceImageLauncher.launch(action)
443+
}
385444
}
386445
}
387446
}
@@ -397,6 +456,7 @@ class SpacesListFragment :
397456
const val DRIVES_READ_WRITE_ALL_PERMISSION = "Drives.ReadWrite.all"
398457
const val DRIVES_READ_WRITE_PROJECT_QUOTA_ALL_PERMISSION = "Drives.ReadWriteProjectQuota.all"
399458
const val DRIVES_DELETE_PROJECT_ALL_PERMISSION = "Drives.DeleteProject.all"
459+
const val SPACE_CONFIG_DIR = "/.space/"
400460

401461
private const val DIALOG_CREATE_SPACE = "DIALOG_CREATE_SPACE"
402462

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.owncloud.android.domain.spaces.model.OCSpace
3333
import com.owncloud.android.domain.spaces.model.SpaceMenuOption
3434
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
3535
import com.owncloud.android.domain.spaces.usecases.DisableSpaceUseCase
36+
import com.owncloud.android.domain.spaces.usecases.EditSpaceImageUseCase
3637
import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase
3738
import com.owncloud.android.domain.spaces.usecases.EnableSpaceUseCase
3839
import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
@@ -66,6 +67,7 @@ class SpacesListViewModel(
6667
private val createSpaceUseCase: CreateSpaceUseCase,
6768
private val filterSpaceMenuOptionsUseCase: FilterSpaceMenuOptionsUseCase,
6869
private val editSpaceUseCase: EditSpaceUseCase,
70+
private val editSpaceImageUseCase: EditSpaceImageUseCase,
6971
private val disableSpaceUseCase: DisableSpaceUseCase,
7072
private val enableSpaceUseCase: EnableSpaceUseCase,
7173
private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider,
@@ -92,6 +94,9 @@ class SpacesListViewModel(
9294
private val _editSpaceFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
9395
val editSpaceFlow: SharedFlow<Event<UIResult<Unit>>?> = _editSpaceFlow
9496

97+
private val _editSpaceImageFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
98+
val editSpaceImageFlow: SharedFlow<Event<UIResult<Unit>>?> = _editSpaceImageFlow
99+
95100
private val _disableSpaceFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
96101
val disableSpaceFlow: SharedFlow<Event<UIResult<Unit>>?> = _disableSpaceFlow
97102

@@ -224,6 +229,14 @@ class SpacesListViewModel(
224229
)
225230
}
226231

232+
fun editSpaceImage(spaceId: String, remotePath: String) {
233+
runSpaceOperation(
234+
flow = _editSpaceImageFlow,
235+
useCase = editSpaceImageUseCase,
236+
useCaseParams = EditSpaceImageUseCase.Params(accountName, spaceId, remotePath)
237+
)
238+
}
239+
227240
private fun <Params> runSpaceOperation(
228241
flow: MutableSharedFlow<Event<UIResult<Unit>>?>,
229242
useCase: BaseUseCaseWithResult<Unit, Params>,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,9 @@
851851
<string name="edit_space">Edit space</string>
852852
<string name="edit_space_correctly">Space updated correctly</string>
853853
<string name="edit_space_failed">Space could not be updated</string>
854+
<string name="edit_space_image">Edit image</string>
855+
<string name="edit_space_image_correctly">Image updated correctly</string>
856+
<string name="edit_space_image_failed">Image could not be updated</string>
854857
<string name="disable_space">Disable space</string>
855858
<string name="disable_space_dialog_title">Do you really want to disable the space: %1$s?</string>
856859
<string name="disable_space_dialog_message">If you disable the selected space, it can no longer be accessed. Only Space managers will still have access. Note: No files will be deleted from the server.</string>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.JSONArray
37+
import org.json.JSONObject
38+
import timber.log.Timber
39+
import java.net.URL
40+
41+
class EditRemoteSpaceImageOperation(
42+
private val spaceId: String,
43+
private val imageId: String
44+
): RemoteOperation<SpaceResponse>() {
45+
override fun run(client: OwnCloudClient): RemoteOperationResult<SpaceResponse> {
46+
var result: RemoteOperationResult<SpaceResponse>
47+
try {
48+
val moshi = Moshi.Builder().build()
49+
50+
val uriBuilder = client.baseUri.buildUpon().apply {
51+
appendEncodedPath(GRAPH_API_SPACES_PATH)
52+
appendEncodedPath(spaceId)
53+
}
54+
55+
val specialFolder = JSONObject().apply {
56+
put(SPACE_NAME_BODY_PARAM, SPACE_NAME_BODY_PARAM_VALUE)
57+
}
58+
59+
val specialEntry = JSONObject().apply {
60+
put(SPACE_ID_BODY_PARAM, imageId)
61+
put(SPACE_SPECIAL_FOLDER_BODY_PARAM, specialFolder)
62+
}
63+
64+
val requestBody = JSONObject().apply {
65+
put(SPACE_SPECIAL_BODY_PARAM, JSONArray().apply { put(specialEntry) })
66+
}.toString().toRequestBody(CONTENT_TYPE_JSON.toMediaType())
67+
68+
69+
val patchMethod = PatchMethod(URL(uriBuilder.build().toString()), requestBody)
70+
71+
val status = client.executeHttpMethod(patchMethod)
72+
73+
val response = patchMethod.getResponseBodyAsString()
74+
75+
if (status == HttpConstants.HTTP_OK) {
76+
Timber.d("Successful response: $response")
77+
78+
val responseAdapter: JsonAdapter<SpaceResponse> = moshi.adapter(SpaceResponse::class.java)
79+
80+
result = RemoteOperationResult(ResultCode.OK)
81+
result.data = responseAdapter.fromJson(response)
82+
83+
Timber.d("Update of space completed and parsed to ${result.data}")
84+
} else {
85+
result = RemoteOperationResult(patchMethod)
86+
Timber.e("Failed response while updating the space; status code: $status, response: $response")
87+
}
88+
} catch (e: Exception) {
89+
result = RemoteOperationResult(e)
90+
Timber.e(e, "Exception while updating the space $spaceId")
91+
}
92+
return result
93+
}
94+
95+
companion object {
96+
private const val GRAPH_API_SPACES_PATH = "graph/v1.0/drives/"
97+
private const val SPACE_SPECIAL_BODY_PARAM = "special"
98+
private const val SPACE_ID_BODY_PARAM = "id"
99+
private const val SPACE_SPECIAL_FOLDER_BODY_PARAM = "specialFolder"
100+
private const val SPACE_NAME_BODY_PARAM = "name"
101+
private const val SPACE_NAME_BODY_PARAM_VALUE = "image"
102+
}
103+
104+
}

owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/services/OCSpacesService.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.owncloud.android.lib.common.OwnCloudClient
2626
import com.owncloud.android.lib.common.operations.RemoteOperationResult
2727
import com.owncloud.android.lib.resources.spaces.CreateRemoteSpaceOperation
2828
import com.owncloud.android.lib.resources.spaces.DisableRemoteSpaceOperation
29+
import com.owncloud.android.lib.resources.spaces.EditRemoteSpaceImageOperation
2930
import com.owncloud.android.lib.resources.spaces.EditRemoteSpaceOperation
3031
import com.owncloud.android.lib.resources.spaces.EnableRemoteSpaceOperation
3132
import com.owncloud.android.lib.resources.spaces.GetRemoteSpacePermissionsOperation
@@ -45,6 +46,9 @@ class OCSpacesService(override val client: OwnCloudClient) : SpacesService {
4546
override fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult<SpaceResponse> =
4647
EditRemoteSpaceOperation(spaceId, spaceName, spaceSubtitle, spaceQuota).execute(client)
4748

49+
override fun editSpaceImage(spaceId: String, imageId: String): RemoteOperationResult<SpaceResponse> =
50+
EditRemoteSpaceImageOperation(spaceId, imageId).execute(client)
51+
4852
override fun disableSpace(spaceId: String, deleteMode: Boolean): RemoteOperationResult<Unit> =
4953
DisableRemoteSpaceOperation(spaceId, deleteMode).execute(client)
5054

0 commit comments

Comments
 (0)