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
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,8 @@ class DialogFragmentIT : AbstractIT() {
fda,
user,
hoverCard,
fda.viewThemeUtils
fda.viewThemeUtils,
fda.avatarGenerator
)
sut.show()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.utils.avatar.AvatarGenerationListener
import com.nextcloud.utils.avatar.AvatarGenerator
import com.nextcloud.utils.extensions.getParcelableArgument
import com.nextcloud.utils.mdm.MDMConfig
import com.owncloud.android.R
Expand All @@ -36,7 +38,6 @@ import com.owncloud.android.ui.activity.DrawerActivity
import com.owncloud.android.ui.adapter.UserListAdapter
import com.owncloud.android.ui.adapter.UserListItem
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener
import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlinx.coroutines.launch
import javax.inject.Inject
Expand Down Expand Up @@ -64,6 +65,9 @@ class ChooseAccountDialogFragment :
@Inject
lateinit var viewThemeUtils: ViewThemeUtils

@Inject
lateinit var avatarGenerator: AvatarGenerator

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
Expand Down Expand Up @@ -92,13 +96,11 @@ class ChooseAccountDialogFragment :

// Defining user picture
binding.currentAccount.userIcon.tag = user.accountName
DisplayUtils.setAvatar(
avatarGenerator.setAccountAvatar(
user,
this,
resources.getDimension(R.dimen.list_item_avatar_icon_radius),
resources,
binding.currentAccount.userIcon,
context
binding.currentAccount.userIcon
)

// Defining user texts, accounts, etc.
Expand All @@ -119,7 +121,8 @@ class ChooseAccountDialogFragment :
false,
false,
true,
viewThemeUtils
viewThemeUtils,
avatarGenerator
)

if (!MDMConfig.multiAccountSupport(requireContext())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import com.nextcloud.client.account.CurrentAccountProvider
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.di.ViewModelFactory
import com.nextcloud.utils.SnackbarUtil
import com.nextcloud.utils.avatar.AvatarGenerationListener
import com.nextcloud.utils.avatar.AvatarGenerator
import com.nextcloud.utils.extensions.setVisibleIf
import com.nextcloud.utils.thumbnail.ThumbnailArguments
import com.owncloud.android.R
Expand All @@ -46,7 +48,6 @@ import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.lib.resources.files.model.FileLockType
import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener
import com.owncloud.android.utils.FileStorageUtils
import com.nextcloud.utils.thumbnail.ThumbnailGenerator
import com.owncloud.android.utils.theme.ViewThemeUtils
Expand Down Expand Up @@ -74,6 +75,9 @@ class FileActionsBottomSheet :
@Inject
lateinit var thumbnailGenerator: ThumbnailGenerator

@Inject
lateinit var avatarGenerator: AvatarGenerator

private lateinit var viewModel: FileActionsViewModel

private var _binding: FileActionsBottomSheetBinding? = null
Expand Down Expand Up @@ -279,14 +283,11 @@ class FileActionsBottomSheet :

override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean = false
}
DisplayUtils.setAvatar(
currentUserProvider.user,
avatarGenerator.setUserAvatar(
lockInfo.lockedBy,
listener,
resources.getDimension(R.dimen.list_item_avatar_icon_radius),
resources,
this,
requireContext()
this
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.avatar

import android.graphics.drawable.Drawable

interface AvatarGenerationListener {
fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?)

fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean
}
124 changes: 124 additions & 0 deletions app/src/main/java/com/nextcloud/utils/avatar/AvatarGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.avatar

import android.accounts.AccountManager
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.nextcloud.client.account.CurrentAccountProvider
import com.nextcloud.client.account.User
import com.nextcloud.client.di.ApplicationScope
import com.nextcloud.client.di.IoDispatcher
import com.nextcloud.client.di.MainDispatcher
import com.owncloud.android.R
import com.owncloud.android.lib.common.accounts.AccountUtils
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.TextDrawable
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference
import javax.inject.Inject
import javax.inject.Singleton

@Suppress("LongParameterList")
@Singleton
class AvatarGenerator @Inject constructor(
private val context: Context,
private val accountManager: AccountManager,
private val currentAccountProvider: CurrentAccountProvider,
private val avatarLoader: AvatarLoader,
@param:ApplicationScope private val scope: CoroutineScope,
@param:IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@param:MainDispatcher private val mainDispatcher: CoroutineDispatcher
) {

companion object {
private const val TAG = "AvatarGenerator"
private const val MAX_PARALLEL_DOWNLOADS = 4
}

private val downloadDispatcher = ioDispatcher.limitedParallelism(MAX_PARALLEL_DOWNLOADS)

fun setAccountAvatar(user: User, listener: AvatarGenerationListener, avatarRadius: Float, callContext: Any) {
val userId = accountManager.getUserData(user.toPlatformAccount(), AccountUtils.Constants.KEY_USER_ID)

if (userId == null) {
Log_OC.e(TAG, "user id is null, cannot set avatar")
return
}

setUserAvatar(userId, listener, avatarRadius, callContext, user = user)
}

@JvmOverloads
fun setUserAvatar(
userId: String,
listener: AvatarGenerationListener,
avatarRadius: Float,
callContext: Any,
displayName: String? = userId,
user: User = currentAccountProvider.user,
avatarBorder: Int = 0
) {
(callContext as? View)?.contentDescription = user.toPlatformAccount().hashCode().toString()

val request = AvatarRequest(user, userId, displayName, avatarRadius)
val hasUserId = userId.isNotEmpty()

val placeholder = if (hasUserId) {
createInitialAvatar(displayName, avatarRadius - avatarBorder)
} else {
createLinkAvatar(callContext)
}
listener.avatarGenerated(placeholder, callContext)

val listenerReference = WeakReference(listener)
scope.launch(ioDispatcher) {
if (hasUserId) {
avatarLoader.loadCached(request)?.let { deliver(listenerReference, request, it, callContext) }
}

val avatar = withContext(downloadDispatcher) { avatarLoader.load(request) } ?: return@launch
deliver(listenerReference, request, avatar, callContext)
}
}

private suspend fun deliver(
listenerReference: WeakReference<AvatarGenerationListener>,
request: AvatarRequest,
avatar: Drawable,
callContext: Any
) = withContext(mainDispatcher) {
val listener = listenerReference.get() ?: return@withContext

if (listener.shouldCallGeneratedCallback(request.accountName, callContext)) {
listener.avatarGenerated(avatar, callContext)
}
}

@Suppress("TooGenericExceptionCaught")
private fun createInitialAvatar(displayName: String?, radius: Float): Drawable? = try {
TextDrawable.createAvatarByUserId(displayName, radius)
} catch (e: Exception) {
Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e)
ResourcesCompat.getDrawable(context.resources, R.drawable.account_circle_white, null)
}

private fun createLinkAvatar(callContext: Any): Drawable? {
val themedContext = (callContext as? View)?.context ?: context

return ContextCompat.getDrawable(themedContext, R.drawable.ic_link)?.apply {
setTint(ContextCompat.getColor(themedContext, R.color.icon_on_nc_grey))
}
}
}
Loading
Loading