Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ ownCloud admins and users.
## Summary

* Change - Migrate tests to the new kotlinx-coroutines-test API: [#4710](https://github.com/owncloud/android/issues/4710)
* Enhancement - Set emoji as space image: [#4707](https://github.com/owncloud/android/issues/4707)

## Details

Expand All @@ -52,6 +53,15 @@ ownCloud admins and users.
https://github.com/owncloud/android/issues/4710
https://github.com/owncloud/android/pull/4722

* Enhancement - Set emoji as space image: [#4707](https://github.com/owncloud/android/issues/4707)

A new option to set an emoji as 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/4707
https://github.com/owncloud/android/pull/4708

# Changelog for ownCloud Android Client [4.7.0] (2025-11-17)

The following sections list the changes in ownCloud Android Client 4.7.0 relevant to
Expand Down
7 changes: 7 additions & 0 deletions changelog/unreleased/4708
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Set emoji as space image

A new option to set an emoji as 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/4707
https://github.com/owncloud/android/pull/4708
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ androidxBiometric = "1.1.0"
androidxBrowser = "1.5.0"
androidxContraintLayout = "2.1.4"
androidxCore = "1.10.1"
androidxEmojiPicker = "1.6.0"
androidxEnterpriseFeedback = "1.1.0"
androidxEspresso = "3.5.1"
androidxFragment = "1.5.7"
Expand Down Expand Up @@ -55,6 +56,7 @@ androidx-biometric = { group = "androidx.biometric", name = "biometric", version
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidxContraintLayout" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
androidx-emoji2-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "androidxEmojiPicker" }
androidx-enterprise-feedback = { group = "androidx.enterprise", name = "enterprise-feedback", version.ref = "androidxEnterpriseFeedback" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidxFragment" }
androidx-fragment-testing = { group = "androidx.fragment", name = "fragment-testing", version.ref = "androidxFragment" }
Expand Down
1 change: 1 addition & 0 deletions owncloudApp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
implementation libs.androidx.work.runtime.ktx
implementation(libs.androidx.browser) { because "CustomTabs required for OAuth2 and OIDC" }
implementation(libs.androidx.enterprise.feedback) { because "MDM feedback" }
implementation libs.androidx.emoji2.emojipicker

// Image loading
implementation libs.coil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCa
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.transfers.usecases.ClearSuccessfulTransferByIdUseCase
import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase
import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsStreamUseCase
import com.owncloud.android.domain.transfers.usecases.GetAllTransfersUseCase
Expand Down Expand Up @@ -254,6 +255,7 @@ val useCaseModule = module {
factoryOf(::CancelUploadUseCase)
factoryOf(::CancelUploadsRecursivelyUseCase)
factoryOf(::ClearFailedTransfersUseCase)
factoryOf(::ClearSuccessfulTransferByIdUseCase)
factoryOf(::ClearSuccessfulTransfersUseCase)
factoryOf(::DownloadFileUseCase)
factoryOf(::GetAllTransfersAsStreamUseCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ val viewModelModule = module {
viewModel { AuthenticationViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
viewModel { MigrationViewModel(MainApp.dataFolder, get(), get(), get(), get(), get(), get(), get()) }
viewModel { TransfersViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
get()) }
get(), 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(), get(), get(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fun SpaceMenuOption.toStringResId() =
SpaceMenuOption.DISABLE -> R.string.disable_space
SpaceMenuOption.ENABLE -> R.string.enable_space
SpaceMenuOption.DELETE -> R.string.delete_space
SpaceMenuOption.SET_ICON -> R.string.set_space_icon
}

fun SpaceMenuOption.toDrawableResId() =
Expand All @@ -39,4 +40,5 @@ fun SpaceMenuOption.toDrawableResId() =
SpaceMenuOption.DISABLE -> R.drawable.ic_disable_space
SpaceMenuOption.ENABLE -> R.drawable.ic_enable_space
SpaceMenuOption.DELETE -> R.drawable.ic_action_delete_white
SpaceMenuOption.SET_ICON -> R.drawable.ic_set_space_icon
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class ReleaseNotesViewModel(
subtitle = R.string.release_notes_4_7_0_subtitle_new_layout_for_spaces,
type = ReleaseNoteType.ENHANCEMENT
),
ReleaseNote(
title = R.string.release_notes_4_8_0_title_set_emoji_as_space_image,
subtitle = R.string.release_notes_4_8_0_subtitle_set_emoji_as_space_image,
type = ReleaseNoteType.ENHANCEMENT
),
ReleaseNote(
title = R.string.release_notes_bugfixes_title,
subtitle = R.string.release_notes_bugfixes_subtitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ 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 com.owncloud.android.presentation.spaces.setspaceicon.SetSpaceIconDialogFragment
import kotlinx.coroutines.flow.SharedFlow
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
Expand All @@ -82,16 +83,16 @@ class SpacesListFragment :
SpacesListAdapter.SpacesListAdapterListener,
Fragment(),
SearchView.OnQueryTextListener,
CreateSpaceDialogFragment.CreateSpaceListener
CreateSpaceDialogFragment.CreateSpaceListener,
SetSpaceIconDialogFragment.SetIconListener
{
private var _binding: SpacesListFragmentBinding? = null
private val binding get() = _binding!!

private var isMultiPersonal = false
private var userPermissions = mutableSetOf<UserPermissions>()
private var editQuotaPermission = false
private var lastUpdatedRemotePath: String? = null
private var selectedImageName: String? = null
private var selectedImagePath: String? = null
private lateinit var currentSpace: OCSpace

private val spacesListViewModel: SpacesListViewModel by viewModel {
Expand All @@ -114,7 +115,7 @@ class SpacesListFragment :
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
selectedImagePath = SPACE_CONFIG_DIR + documentFile.name

transfersViewModel.uploadFilesFromContentUri(
accountName = accountName,
Expand Down Expand Up @@ -247,14 +248,15 @@ class SpacesListFragment :
}

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

if (matchedTransfer != null && lastUpdatedRemotePath != matchedTransfer.remotePath) {
matchedTransfer?.let {
when(matchedTransfer.status) {
TransferStatus.TRANSFER_SUCCEEDED -> {
spacesListViewModel.editSpaceImage(currentSpace.id, matchedTransfer.remotePath)
lastUpdatedRemotePath = matchedTransfer.remotePath
matchedTransfer.id?.let {
transfersViewModel.clearSuccessfulTransferById(it)
}
}
TransferStatus.TRANSFER_FAILED -> {
showMessageInSnackbar(getString(R.string.edit_space_image_failed))
Expand Down Expand Up @@ -328,6 +330,17 @@ class SpacesListFragment :
spacesListViewModel.editSpace(spaceId, spaceName, spaceSubtitle, spaceQuota)
}

override fun setSpaceIcon(fileName: String, localPath: String) {
selectedImagePath = SPACE_CONFIG_DIR + fileName
transfersViewModel.uploadFilesFromSystem(
accountName = requireArguments().getString(BUNDLE_ACCOUNT_NAME) ?: "",
listOfLocalPaths = listOf(localPath),
uploadFolderPath = SPACE_CONFIG_DIR,
spaceId = currentSpace.id,
forceOverwrite = true
)
}

fun setSearchListener(searchView: SearchView) {
searchView.setOnQueryTextListener(this)
}
Expand Down Expand Up @@ -444,6 +457,10 @@ class SpacesListFragment :
}
editSpaceImageLauncher.launch(action)
}
SpaceMenuOption.SET_ICON -> {
val setIconDialog = SetSpaceIconDialogFragment.newInstance(listener = this@SpacesListFragment)
setIconDialog.show(requireActivity().supportFragmentManager, DIALOG_SET_ICON)
}
}
}
}
Expand All @@ -467,6 +484,7 @@ class SpacesListFragment :
const val SPACE_CONFIG_DIR = "/.space/"

private const val DIALOG_CREATE_SPACE = "DIALOG_CREATE_SPACE"
private const val DIALOG_SET_ICON = "DIALOG_SET_ICON"

fun newInstance(
showPersonalSpace: Boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* 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.presentation.spaces.setspaceicon

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.owncloud.android.databinding.SetSpaceIconDialogBinding
import java.io.File

class SetSpaceIconDialogFragment : DialogFragment() {
private var _binding: SetSpaceIconDialogBinding? = null
private val binding get() = _binding!!

private lateinit var setIconListener: SetIconListener

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = SetSpaceIconDialogBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
cancelSetSpaceIconButton.setOnClickListener { dialog?.dismiss() }
emojiPicker.setOnEmojiPickedListener { emojiItem ->
val emojiFile = convertEmojiToImageFile(emojiItem.emoji)
setIconListener.setSpaceIcon(emojiFile.name, emojiFile.absolutePath)
dialog?.dismiss()
}
}
}

private fun convertEmojiToImageFile(emoji: String): File {
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
textSize = EMOJI_SIZE
textAlign = Paint.Align.CENTER
}
val bitmap = Bitmap.createBitmap(ICON_WIDTH, ICON_HEIGHT, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)

val bounds = Rect()
paint.getTextBounds(emoji, 0, emoji.length, bounds)
val baseline = (ICON_HEIGHT / 2f) - bounds.exactCenterY()

canvas.drawText(emoji, (ICON_WIDTH / 2f), baseline, paint)

val file = File(requireContext().cacheDir, EMOJI_FILE_NAME)
file.outputStream().use { output ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
}
return file
}

interface SetIconListener {
fun setSpaceIcon(fileName: String, localPath: String)
}

companion object {
private const val ICON_HEIGHT = 405
private const val ICON_WIDTH = 720
private const val EMOJI_SIZE = 250f
private const val EMOJI_FILE_NAME = "emoji.png"

fun newInstance(
listener: SetIconListener
): SetSpaceIconDialogFragment =
SetSpaceIconDialogFragment().apply {
setIconListener = listener
arguments = Bundle()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,6 +31,7 @@ import com.owncloud.android.domain.files.model.OCFile
import com.owncloud.android.domain.spaces.model.OCSpace
import com.owncloud.android.domain.spaces.usecases.GetSpacesFromEveryAccountUseCaseAsStream
import com.owncloud.android.domain.transfers.model.OCTransfer
import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransferByIdUseCase
import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase
import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsStreamUseCase
import com.owncloud.android.providers.CoroutinesDispatcherProvider
Expand Down Expand Up @@ -61,6 +63,7 @@ class TransfersViewModel(
private val retryFailedUploadsForAccountUseCase: RetryFailedUploadsForAccountUseCase,
private val clearFailedTransfersUseCase: ClearFailedTransfersUseCase,
private val retryFailedUploadsUseCase: RetryFailedUploadsUseCase,
private val clearSuccessfulTransferByIdUseCase: ClearSuccessfulTransferByIdUseCase,
private val clearSuccessfulTransfersUseCase: ClearSuccessfulTransfersUseCase,
getAllTransfersAsStreamUseCase: GetAllTransfersAsStreamUseCase,
private val cancelDownloadForFileUseCase: CancelDownloadForFileUseCase,
Expand Down Expand Up @@ -120,6 +123,7 @@ class TransfersViewModel(
listOfLocalPaths: List<String>,
uploadFolderPath: String,
spaceId: String?,
forceOverwrite: Boolean = false
) {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
uploadFilesFromSystemUseCase(
Expand All @@ -128,6 +132,7 @@ class TransfersViewModel(
listOfLocalPaths = listOfLocalPaths,
uploadFolderPath = uploadFolderPath,
spaceId = spaceId,
forceOverwrite = forceOverwrite
)
)
}
Expand Down Expand Up @@ -196,4 +201,10 @@ class TransfersViewModel(
clearSuccessfulTransfersUseCase(Unit)
}
}

fun clearSuccessfulTransferById(id: Long) {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
clearSuccessfulTransferByIdUseCase(ClearSuccessfulTransferByIdUseCase.Params(id))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ private void showUploadTextDialog() {
fileToUpload.add(filePath);
@NotNull Lazy<TransfersViewModel> transfersViewModelLazy = inject(TransfersViewModel.class);
TransfersViewModel transfersViewModel = transfersViewModelLazy.getValue();
transfersViewModel.uploadFilesFromSystem(getAccount().name, fileToUpload, mUploadPath, currentSpaceId);
transfersViewModel.uploadFilesFromSystem(getAccount().name, fileToUpload, mUploadPath, currentSpaceId, false);
finish();

inputLayout.setErrorEnabled(error != null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ protected ResultCode doInBackground(Object[] params) {
account.name,
filesToUpload,
uploadPath,
spaceId
spaceId,
false
);
uploadFilesFromSystemUseCase.invoke(useCaseParams);
fullTempPath = null;
Expand Down
Loading
Loading