Skip to content
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ ownCloud admins and users.
* 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 - Disable/Remove a space: [#4611](https://github.com/owncloud/android/issues/4611)
* Enhancement - Update space image: [#4691](https://github.com/owncloud/android/issues/4691)
* Enhancement - Show space quota: [#4693](https://github.com/owncloud/android/issues/4693)
* Enhancement - Add user role to spaces: [#4698](https://github.com/owncloud/android/pull/4698)

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

* Enhancement - Update space image: [#4691](https://github.com/owncloud/android/issues/4691)

A new option to update the space image 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/4691
https://github.com/owncloud/android/pull/4705

* Enhancement - Show space quota: [#4693](https://github.com/owncloud/android/issues/4693)

The used and total values of the space quota have been added to the bottom sheet
Expand Down
7 changes: 7 additions & 0 deletions changelog/unreleased/4705
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Update space image

A new option to update the space image 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/4691
https://github.com/owncloud/android/pull/4705
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUs
import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.DisableSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.EditSpaceImageUseCase
import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.EnableSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
Expand Down Expand Up @@ -229,6 +230,7 @@ val useCaseModule = module {
// Spaces
factoryOf(::CreateSpaceUseCase)
factoryOf(::DisableSpaceUseCase)
factoryOf(::EditSpaceImageUseCase)
factoryOf(::EditSpaceUseCase)
factoryOf(::EnableSpaceUseCase)
factoryOf(::FilterSpaceMenuOptionsUseCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ 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(), get(), get(), get(), accountName,
SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), accountName,
showPersonalSpace)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.owncloud.android.domain.spaces.model.SpaceMenuOption
fun SpaceMenuOption.toStringResId() =
when (this) {
SpaceMenuOption.EDIT -> R.string.edit_space
SpaceMenuOption.EDIT_IMAGE -> R.string.edit_space_image
SpaceMenuOption.DISABLE -> R.string.disable_space
SpaceMenuOption.ENABLE -> R.string.enable_space
SpaceMenuOption.DELETE -> R.string.delete_space
Expand All @@ -34,6 +35,7 @@ fun SpaceMenuOption.toStringResId() =
fun SpaceMenuOption.toDrawableResId() =
when (this) {
SpaceMenuOption.EDIT -> R.drawable.ic_pencil
SpaceMenuOption.EDIT_IMAGE -> R.drawable.file_image
SpaceMenuOption.DISABLE -> R.drawable.ic_disable_space
SpaceMenuOption.ENABLE -> R.drawable.ic_enable_space
SpaceMenuOption.DELETE -> R.drawable.ic_action_delete_white
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@

package com.owncloud.android.presentation.spaces

import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.SearchView
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.recyclerview.widget.GridLayoutManager
Expand All @@ -43,8 +47,15 @@ import com.owncloud.android.R
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.files.model.MIME_BMP
import com.owncloud.android.domain.files.model.MIME_GIF
import com.owncloud.android.domain.files.model.MIME_JPEG
import com.owncloud.android.domain.files.model.MIME_PNG
import com.owncloud.android.domain.files.model.MIME_PREFIX_IMAGE
import com.owncloud.android.domain.files.model.MIME_X_MS_BMP
import com.owncloud.android.domain.spaces.model.OCSpace
import com.owncloud.android.domain.spaces.model.SpaceMenuOption
import com.owncloud.android.domain.transfers.model.TransferStatus
import com.owncloud.android.domain.user.model.UserPermissions
import com.owncloud.android.domain.utils.Event
import com.owncloud.android.extensions.collectLatestLifecycleFlow
Expand All @@ -61,6 +72,7 @@ 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
import com.owncloud.android.presentation.transfers.TransfersViewModel
import kotlinx.coroutines.flow.SharedFlow
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
Expand All @@ -78,6 +90,8 @@ class SpacesListFragment :
private var isMultiPersonal = false
private var userPermissions = mutableSetOf<UserPermissions>()
private var editQuotaPermission = false
private var lastUpdatedRemotePath: String? = null
private var selectedImageName: String? = null
private lateinit var currentSpace: OCSpace

private val spacesListViewModel: SpacesListViewModel by viewModel {
Expand All @@ -91,6 +105,24 @@ class SpacesListFragment :
requireArguments().getString(BUNDLE_ACCOUNT_NAME),
)
}
private val transfersViewModel: TransfersViewModel by viewModel()

private val editSpaceImageLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult

val selectedImageUri = result.data?.data ?: return@registerForActivityResult
val accountName = requireArguments().getString(BUNDLE_ACCOUNT_NAME) ?: return@registerForActivityResult
val documentFile = DocumentFile.fromSingleUri(requireContext(), selectedImageUri) ?: return@registerForActivityResult
selectedImageName = documentFile.name

transfersViewModel.uploadFilesFromContentUri(
accountName = accountName,
listOfContentUris = listOf(selectedImageUri),
uploadFolderPath = SPACE_CONFIG_DIR,
spaceId = currentSpace.id
)
}

private lateinit var spacesListAdapter: SpacesListAdapter

Expand Down Expand Up @@ -202,6 +234,7 @@ class SpacesListFragment :

collectSpaceOperationsFlow(spacesListViewModel.createSpaceFlow, R.string.create_space_correctly, R.string.create_space_failed)
collectSpaceOperationsFlow(spacesListViewModel.editSpaceFlow, R.string.edit_space_correctly, R.string.edit_space_failed)
collectSpaceOperationsFlow(spacesListViewModel.editSpaceImageFlow, R.string.edit_space_image_correctly, R.string.edit_space_image_failed)
collectSpaceOperationsFlow(spacesListViewModel.disableSpaceFlow, R.string.disable_space_correctly, R.string.disable_space_failed)
collectSpaceOperationsFlow(spacesListViewModel.enableSpaceFlow, R.string.enable_space_correctly, R.string.enable_space_failed)
collectSpaceOperationsFlow(spacesListViewModel.deleteSpaceFlow, R.string.delete_space_correctly, R.string.delete_space_failed)
Expand All @@ -210,6 +243,17 @@ class SpacesListFragment :
showSpaceMenuOptionsDialog(menuOptions)
}

collectLatestLifecycleFlow(transfersViewModel.transfersWithSpaceStateFlow) { transfersWithSpace ->
val remotePath = SPACE_CONFIG_DIR + selectedImageName
val matchedTransfer = transfersWithSpace.map { it.first }.find { it.remotePath == remotePath }

if (matchedTransfer != null && matchedTransfer.status == TransferStatus.TRANSFER_SUCCEEDED &&
lastUpdatedRemotePath != matchedTransfer.remotePath) {
spacesListViewModel.editSpaceImage(currentSpace.id, matchedTransfer.remotePath)
lastUpdatedRemotePath = matchedTransfer.remotePath
}
}

}

private fun collectSpaceOperationsFlow(flow: SharedFlow<Event<UIResult<Unit>>?>, successMessage: Int, errorMessage: Int) {
Expand Down Expand Up @@ -382,6 +426,14 @@ class SpacesListFragment :
negativeButtonText = getString(R.string.common_no)
)
}
SpaceMenuOption.EDIT_IMAGE -> {
val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = MIME_PREFIX_IMAGE
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(MIME_JPEG, MIME_PNG, MIME_BMP, MIME_X_MS_BMP, MIME_GIF))
}
editSpaceImageLauncher.launch(action)
}
}
}
}
Expand All @@ -397,6 +449,7 @@ class SpacesListFragment :
const val DRIVES_READ_WRITE_ALL_PERMISSION = "Drives.ReadWrite.all"
const val DRIVES_READ_WRITE_PROJECT_QUOTA_ALL_PERMISSION = "Drives.ReadWriteProjectQuota.all"
const val DRIVES_DELETE_PROJECT_ALL_PERMISSION = "Drives.DeleteProject.all"
const val SPACE_CONFIG_DIR = "/.space/"

private const val DIALOG_CREATE_SPACE = "DIALOG_CREATE_SPACE"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ 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.DisableSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.EditSpaceImageUseCase
import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.EnableSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
Expand Down Expand Up @@ -66,6 +67,7 @@ class SpacesListViewModel(
private val createSpaceUseCase: CreateSpaceUseCase,
private val filterSpaceMenuOptionsUseCase: FilterSpaceMenuOptionsUseCase,
private val editSpaceUseCase: EditSpaceUseCase,
private val editSpaceImageUseCase: EditSpaceImageUseCase,
private val disableSpaceUseCase: DisableSpaceUseCase,
private val enableSpaceUseCase: EnableSpaceUseCase,
private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider,
Expand All @@ -92,6 +94,9 @@ class SpacesListViewModel(
private val _editSpaceFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
val editSpaceFlow: SharedFlow<Event<UIResult<Unit>>?> = _editSpaceFlow

private val _editSpaceImageFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
val editSpaceImageFlow: SharedFlow<Event<UIResult<Unit>>?> = _editSpaceImageFlow

private val _disableSpaceFlow = MutableSharedFlow<Event<UIResult<Unit>>?>()
val disableSpaceFlow: SharedFlow<Event<UIResult<Unit>>?> = _disableSpaceFlow

Expand Down Expand Up @@ -224,6 +229,14 @@ class SpacesListViewModel(
)
}

fun editSpaceImage(spaceId: String, remotePath: String) {
runSpaceOperation(
flow = _editSpaceImageFlow,
useCase = editSpaceImageUseCase,
useCaseParams = EditSpaceImageUseCase.Params(accountName, spaceId, remotePath)
)
}

private fun <Params> runSpaceOperation(
flow: MutableSharedFlow<Event<UIResult<Unit>>?>,
useCase: BaseUseCaseWithResult<Unit, Params>,
Expand Down
3 changes: 3 additions & 0 deletions owncloudApp/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,9 @@
<string name="edit_space">Edit space</string>
<string name="edit_space_correctly">Space updated correctly</string>
<string name="edit_space_failed">Space could not be updated</string>
<string name="edit_space_image">Edit image</string>
<string name="edit_space_image_correctly">Image updated correctly</string>
<string name="edit_space_image_failed">Image could not be updated</string>
<string name="disable_space">Disable space</string>
<string name="disable_space_dialog_title">Do you really want to disable the space: %1$s?</string>
<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>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/


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.JSONArray
import org.json.JSONObject
import timber.log.Timber
import java.net.URL

class EditRemoteSpaceImageOperation(
private val spaceId: String,
private val imageId: String
): RemoteOperation<SpaceResponse>() {
override fun run(client: OwnCloudClient): RemoteOperationResult<SpaceResponse> {
var result: RemoteOperationResult<SpaceResponse>
try {
val moshi = Moshi.Builder().build()

val uriBuilder = client.baseUri.buildUpon().apply {
appendEncodedPath(GRAPH_API_SPACES_PATH)
appendEncodedPath(spaceId)
}

val specialFolder = JSONObject().apply {
put(SPACE_NAME_BODY_PARAM, SPACE_NAME_BODY_PARAM_VALUE)
}

val specialEntry = JSONObject().apply {
put(SPACE_ID_BODY_PARAM, imageId)
put(SPACE_SPECIAL_FOLDER_BODY_PARAM, specialFolder)
}

val requestBody = JSONObject().apply {
put(SPACE_SPECIAL_BODY_PARAM, JSONArray().apply { put(specialEntry) })
}.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<SpaceResponse> = 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_SPECIAL_BODY_PARAM = "special"
private const val SPACE_ID_BODY_PARAM = "id"
private const val SPACE_SPECIAL_FOLDER_BODY_PARAM = "specialFolder"
private const val SPACE_NAME_BODY_PARAM = "name"
private const val SPACE_NAME_BODY_PARAM_VALUE = "image"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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.DisableRemoteSpaceOperation
import com.owncloud.android.lib.resources.spaces.EditRemoteSpaceImageOperation
import com.owncloud.android.lib.resources.spaces.EditRemoteSpaceOperation
import com.owncloud.android.lib.resources.spaces.EnableRemoteSpaceOperation
import com.owncloud.android.lib.resources.spaces.GetRemoteSpacePermissionsOperation
Expand All @@ -45,6 +46,9 @@ class OCSpacesService(override val client: OwnCloudClient) : SpacesService {
override fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult<SpaceResponse> =
EditRemoteSpaceOperation(spaceId, spaceName, spaceSubtitle, spaceQuota).execute(client)

override fun editSpaceImage(spaceId: String, imageId: String): RemoteOperationResult<SpaceResponse> =
EditRemoteSpaceImageOperation(spaceId, imageId).execute(client)

override fun disableSpace(spaceId: String, deleteMode: Boolean): RemoteOperationResult<Unit> =
DisableRemoteSpaceOperation(spaceId, deleteMode).execute(client)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface SpacesService : Service {
fun createSpace(spaceName: String, spaceSubtitle: String, spaceQuota: Long): RemoteOperationResult<SpaceResponse>
fun getSpacePermissions(spaceId: String): RemoteOperationResult<List<String>>
fun editSpace(spaceId: String, spaceName: String, spaceSubtitle: String, spaceQuota: Long?): RemoteOperationResult<SpaceResponse>
fun editSpaceImage(spaceId: String, imageId: String): RemoteOperationResult<SpaceResponse>
fun disableSpace(spaceId: String, deleteMode: Boolean): RemoteOperationResult<Unit>
fun enableSpace(spaceId: String): RemoteOperationResult<SpaceResponse>
}
Loading
Loading