From 81d8ee6cc7fbc6be4f9e129a8e2fee847f71665a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 17 Sep 2026 12:06:32 +0200 Subject: [PATCH 1/4] use AvatarShareesProvider Signed-off-by: alperozturk96 --- .../owncloud/android/utils/DisplayUtils.java | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index a678575caba3..49560ab71e08 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -44,12 +44,11 @@ import com.nextcloud.client.account.User; import com.owncloud.android.MainApp; import com.owncloud.android.R; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.ui.TextDrawable; +import com.owncloud.android.ui.adapter.helper.AvatarShareesProvider; import com.owncloud.android.ui.dialog.SortingOrderDialogFragment; import java.io.BufferedReader; @@ -443,30 +442,24 @@ public static void setAvatar(@NonNull User user, avatar.setTint(tintColor); } } else { - ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); - String eTag = arbitraryDataProvider.getValue(userId + "@" + serverName, ThumbnailsCacheManager.AVATAR); - String avatarKey = "a_" + userId + "_" + serverName + "_" + eTag; - - // first show old one - avatar = BitmapUtils.bitmapToCircularBitmapDrawable(resources, - ThumbnailsCacheManager.getBitmapFromDiskCache(avatarKey)); - - // if no one exists, show colored icon with initial char - if (avatar == null) { - try { - avatar = TextDrawable.createAvatarByUserId(displayName, - (avatarRadius - avatarBorder)); - } catch (Exception e) { - Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e); - avatar = ResourcesCompat.getDrawable(resources, - R.drawable.account_circle_white, - null); - } + // show colored icon with initial char until the cached avatar has been read from disk + try { + avatar = TextDrawable.createAvatarByUserId(displayName, + (avatarRadius - avatarBorder)); + } catch (Exception e) { + Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e); + avatar = ResourcesCompat.getDrawable(resources, + R.drawable.account_circle_white, + null); } } listener.avatarGenerated(avatar, callContext); + if (!userId.isEmpty()) { + AvatarShareesProvider.showCachedAvatar(userId, serverName, listener, resources, callContext, context); + } + // check for new avatar, eTag is compared, so only new one is downloaded final ThumbnailsCacheManager.AvatarGenerationTask task = new ThumbnailsCacheManager.AvatarGenerationTask(listener, From df1378c0936922f61506baadb7ad0423da0175ad Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 17 Sep 2026 12:06:59 +0200 Subject: [PATCH 2/4] check previously inserted sharees and fix avatar id Signed-off-by: alperozturk96 --- .../owncloud/android/ui/AvatarGroupLayout.kt | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt index 27beb655fb0f..fc893589c707 100644 --- a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt +++ b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt @@ -47,19 +47,42 @@ class AvatarGroupLayout @JvmOverloads constructor( @Px private val overlapPx: Int = DisplayUtils.convertDpToPixel(24f, context) + var boundFileId: Long? = null + set(value) { + if (field != value) { + displayedSharees = null + } + field = value + } + + private var displayedSharees: List? = null + init { checkNotNull(borderDrawable) DrawableCompat.setTint(borderDrawable, ContextCompat.getColor(context, R.color.bg_default)) } @Suppress("LongMethod", "TooGenericExceptionCaught") - fun setAvatars(user: User, sharees: MutableList, viewThemeUtils: ViewThemeUtils) { + fun setAvatars(user: User, sharees: List, viewThemeUtils: ViewThemeUtils) { + if (sharees == displayedSharees) { + return + } + displayedSharees = sharees + val context = getContext() removeAllViews() + + if (sharees.isEmpty()) { + visibility = GONE + return + } + visibility = VISIBLE + var avatarLayoutParams: LayoutParams? val shareeSize = min(sharees.size, MAX_AVATAR_COUNT) val resources = context.resources val avatarRadius = resources.getDimension(R.dimen.list_item_avatar_icon_radius) + val serverName = user.accountName.substringAfterLast('@') var sharee: ShareeUser var avatarCount = 0 @@ -102,7 +125,7 @@ class AvatarGroupLayout @JvmOverloads constructor( ) else -> { - avatar.tag = sharee + avatar.tag = "${sharee.userId}@$serverName" DisplayUtils.setAvatar( user, sharee.userId!!, From 83068616f749832413f756715ad92321c3a86ef2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 17 Sep 2026 12:07:25 +0200 Subject: [PATCH 3/4] introduce AvatarShareesProvider Signed-off-by: alperozturk96 --- .../android/ui/adapter/OCFileListAdapter.java | 20 ++- .../adapter/helper/AvatarShareesProvider.kt | 166 ++++++++++++++++++ .../adapter/helper/OCFileListAdapterHelper.kt | 39 ---- 3 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index a17e690653e4..a027b0e55706 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -57,6 +57,7 @@ import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.ui.activity.DrawerActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; +import com.owncloud.android.ui.adapter.helper.AvatarShareesProvider; import com.owncloud.android.ui.adapter.helper.OCFileListAdapterDataProvider; import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper; import com.owncloud.android.ui.fragment.OCFileListFragment; @@ -135,6 +136,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter recommendedFiles = new ArrayList<>(); private RecommendedFilesAdapter recommendedFilesAdapter; private final OCFileListAdapterHelper helper = new OCFileListAdapterHelper(); + private final AvatarShareesProvider avatarShareesProvider = new AvatarShareesProvider(); private final ThumbnailGenerator thumbnailGenerator; public OCFileListAdapter( @@ -601,6 +603,7 @@ private void bindSharedAvatars(ListItemViewHolder holder, OCFile file) { final var sharedAvatars = holder.getSharedAvatars(); if (!(file.isSharedWithMe() || file.isSharedWithSharee()) || isMultiSelect() || gridView || hideItemOptions) { + sharedAvatars.setBoundFileId(null); sharedAvatars.setVisibility(View.GONE); if (sharedAvatars.getChildCount() > 0) { sharedAvatars.removeAllViews(); @@ -608,14 +611,14 @@ private void bindSharedAvatars(ListItemViewHolder holder, OCFile file) { return; } - sharedAvatars.setVisibility(View.VISIBLE); - if (sharedAvatars.getChildCount() > 0) { - sharedAvatars.removeAllViews(); - } + final long fileId = file.getFileId(); + sharedAvatars.setBoundFileId(fileId); + sharedAvatars.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); - helper.getAvatarSharees(file, user, userId, avatars -> { - sharedAvatars.setAvatars(user, avatars, viewThemeUtils); - sharedAvatars.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); + avatarShareesProvider.get(file, user, userId, avatars -> { + if (Long.valueOf(fileId).equals(sharedAvatars.getBoundFileId())) { + sharedAvatars.setAvatars(user, avatars, viewThemeUtils); + } return Unit.INSTANCE; }); } @@ -1103,6 +1106,7 @@ public void updateFileIndicator(int iconId, OCFile file) { public void cleanup() { ocFileListDelegate.cleanup(); helper.cleanup(); + avatarShareesProvider.cleanup(); } @SuppressLint("NotifyDataSetChanged") @@ -1128,6 +1132,8 @@ public void removeFile(@NonNull OCFile file) { @SuppressLint("NotifyDataSetChanged") public void updateFile(@NonNull OCFile updatedFile) { + avatarShareesProvider.invalidate(updatedFile); + int allIndex = helper.indexOfSameRemoteFile(mFilesAll, updatedFile); if (allIndex != -1) { mFilesAll.set(allIndex, updatedFile); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt new file mode 100644 index 000000000000..37fe382e9934 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt @@ -0,0 +1,166 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.helper + +import android.content.Context +import android.content.res.Resources +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.share.avatar.ShareAvatarRepository +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.client.account.User +import com.nextcloud.utils.extensions.supportsUnifiedShare +import com.nextcloud.utils.extensions.toServerCredentials +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.lib.resources.shares.ShareeUser +import com.owncloud.android.utils.BitmapUtils +import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class AvatarShareesProvider { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private val cache = object : LinkedHashMap>( + CACHE_INITIAL_CAPACITY, + CACHE_LOAD_FACTOR, + true + ) { + override fun removeEldestEntry(eldest: Map.Entry>): Boolean = size > CACHE_MAX_SIZE + } + + private val pendingRequests = mutableMapOf) -> Unit>>() + + private var unifiedShareCredentials: Deferred? = null + + fun invalidate(file: OCFile) { + cache.remove(file.fileId) + } + + fun get(file: OCFile, user: User?, userId: String?, onSharees: (List) -> Unit) { + val fileId = file.fileId + + cache[fileId]?.let { cached -> + onSharees(cached) + return + } + + val localSharees = file.toLocalSharees(userId) + onSharees(localSharees) + + resolveRemoteSharees(file, user, localSharees, onSharees) + } + + private fun resolveRemoteSharees( + file: OCFile, + user: User?, + localSharees: List, + onSharees: (List) -> Unit + ) { + val fileId = file.fileId + + if (user == null) { + cache[fileId] = localSharees + return + } + + pendingRequests[fileId]?.let { waitingCallbacks -> + waitingCallbacks.add(onSharees) + return + } + pendingRequests[fileId] = mutableListOf(onSharees) + + val credentials = unifiedShareCredentials + ?: scope.async { resolveUnifiedShareCredentials(user) }.also { unifiedShareCredentials = it } + + scope.launch { + val sharees = credentials.await()?.let { fetchRemoteSharees(it, file) } ?: localSharees + + withContext(Dispatchers.Main) { + cache[fileId] = sharees + pendingRequests.remove(fileId)?.forEach { callback -> callback(sharees) } + } + } + } + + fun cleanup() { + cache.clear() + pendingRequests.clear() + unifiedShareCredentials = null + scope.coroutineContext.cancelChildren() + } + + private suspend fun resolveUnifiedShareCredentials(user: User): ServerCredentials? = runCatching { + user.toServerCredentials()?.takeIf { it.supportsUnifiedShare() } + }.getOrNull() + + private suspend fun fetchRemoteSharees(credentials: ServerCredentials, file: OCFile): List? = + runCatching { + ShareAvatarRepository(credentials) + .fetchShareAvatars(file.localId.toString()) + ?.toAvatarSharees() + }.getOrNull() + + private fun OCFile.toLocalSharees(userId: String?): List { + val ownerSharee = ownerId + ?.takeIf { it.isNotEmpty() && it != userId } + ?.let { ShareeUser(it, ownerDisplayName, ShareType.USER) } + ?.takeIf { it !in sharees } + + return listOfNotNull(ownerSharee) + sharees.asReversed() + } + + private fun List.toAvatarSharees(): List = asSequence() + .flatMap { share -> share.invitedRecipients } + .distinctBy { recipient -> recipient.value } + .map { recipient -> ShareeUser(recipient.value, recipient.displayName, ShareType.USER) } + .toList() + + companion object { + private const val CACHE_MAX_SIZE = 500 + private const val CACHE_INITIAL_CAPACITY = 64 + private const val CACHE_LOAD_FACTOR = 0.75f + + private val cachedAvatarScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + @JvmStatic + @Suppress("LongParameterList") + fun showCachedAvatar( + userId: String, + serverName: String, + listener: AvatarGenerationListener, + resources: Resources, + callContext: Any, + context: Context + ) { + val accountName = "$userId@$serverName" + + cachedAvatarScope.launch { + val eTag = ArbitraryDataProviderImpl(context) + .getValue(accountName, ThumbnailsCacheManager.AVATAR) + val cachedBitmap = ThumbnailsCacheManager + .getBitmapFromDiskCache("a_${userId}_${serverName}_$eTag") ?: return@launch + val avatar = BitmapUtils.bitmapToCircularBitmapDrawable(resources, cachedBitmap) + + withContext(Dispatchers.Main) { + if (listener.shouldCallGeneratedCallback(accountName, callContext)) { + listener.avatarGenerated(avatar, callContext) + } + } + } + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt index 68bbd807c576..84aaa80c90da 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -7,19 +7,12 @@ package com.owncloud.android.ui.adapter.helper -import com.nextcloud.android.common.ui.share.avatar.ShareAvatarRepository -import com.nextcloud.android.common.ui.share.model.api.share.Share -import com.nextcloud.client.account.User import com.nextcloud.client.database.entity.FileEntity import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.extensions.filterFilenames import com.nextcloud.utils.extensions.isTempFile -import com.nextcloud.utils.extensions.supportsUnifiedShare -import com.nextcloud.utils.extensions.toServerCredentials import com.owncloud.android.MainApp import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.resources.shares.ShareType -import com.owncloud.android.lib.resources.shares.ShareeUser import com.owncloud.android.utils.FileSortOrder import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.CoroutineScope @@ -61,38 +54,6 @@ class OCFileListAdapterHelper { } } - fun getAvatarSharees(file: OCFile, user: User?, userId: String?, onComplete: (List) -> Unit) { - scope.launch { - val credentials = user?.toServerCredentials() - val result = if (credentials != null && credentials.supportsUnifiedShare()) { - ShareAvatarRepository( - credentials - ).fetchShareAvatars(file.localId.toString())?.toAvatarSharees().orEmpty() - } else { - file.toLocalSharees(userId) - } - - withContext(Dispatchers.Main) { - onComplete(result) - } - } - } - - private fun OCFile.toLocalSharees(userId: String?): List { - val ownerSharee = ownerId - ?.takeIf { it.isNotEmpty() && it != userId } - ?.let { ShareeUser(it, ownerDisplayName, ShareType.USER) } - ?.takeIf { it !in sharees } - - return listOfNotNull(ownerSharee) + sharees.asReversed() - } - - private fun List.toAvatarSharees(): List = asSequence() - .flatMap { share -> share.invitedRecipients } - .distinctBy { recipient -> recipient.value } - .map { recipient -> ShareeUser(recipient.value, recipient.displayName, ShareType.USER) } - .toList() - suspend fun prepareFileList( directory: OCFile, dataProvider: OCFileListAdapterDataProvider, From 05afc6c1cf3702f3b459c8ad0c9cdd37f4e2710e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 18 Sep 2026 11:36:23 +0200 Subject: [PATCH 4/4] fetch sharees before the list and use OCFile only for showing Signed-off-by: alperozturk96 --- .../utils/share/UnifiedShareSharees.kt | 93 +++++++++++++++ .../operations/RefreshFolderOperation.java | 3 + .../SynchronizeFolderOperation.java | 3 + .../android/ui/adapter/OCFileListAdapter.java | 10 +- .../adapter/helper/AvatarShareesProvider.kt | 106 +----------------- .../ui/fragment/OCFileListSearchTask.kt | 4 +- .../android/ui/fragment/SharedListFragment.kt | 2 + 7 files changed, 109 insertions(+), 112 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/utils/share/UnifiedShareSharees.kt diff --git a/app/src/main/java/com/nextcloud/utils/share/UnifiedShareSharees.kt b/app/src/main/java/com/nextcloud/utils/share/UnifiedShareSharees.kt new file mode 100644 index 000000000000..abd6bd70ab68 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/share/UnifiedShareSharees.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.share + +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.share.avatar.ShareAvatarRepository +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.client.account.User +import com.nextcloud.utils.extensions.supportsUnifiedShare +import com.nextcloud.utils.extensions.toServerCredentials +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.lib.resources.shares.ShareeUser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap + +/** + * Replaces the sharees PROPFIND reported with the ones the unified share API reports, at the point where the files + * are written, so that readers take them straight from the [OCFile]. + * + * This is the preferred approach only while the unified share system does not expose the legacy sharee data through + * PROPFIND. Once it is backward compatible and ships the sharees with the file listing again, the listing alone + * carries everything the UI needs and these extra requests can go away. + */ +object UnifiedShareSharees { + private const val MAX_CONCURRENT_REQUESTS = 8 + + private val unifiedShareSupport = ConcurrentHashMap() + + suspend fun fill(user: User, files: List) { + if (files.isEmpty()) { + return + } + + withContext(Dispatchers.IO) { + val credentials = user.toServerCredentials() ?: return@withContext + if (!supportsUnifiedShare(user.accountName, credentials)) { + return@withContext + } + + val repository = ShareAvatarRepository(credentials) + val requestLimit = Semaphore(MAX_CONCURRENT_REQUESTS) + + files + .map { file -> + async { + requestLimit.withPermit { + runCatching { repository.fetchSharees(file) } + } + } + } + .awaitAll() + } + } + + @JvmStatic + fun fillBlocking(user: User, files: List) { + runCatching { + runBlocking { fill(user, files) } + } + } + + private suspend fun supportsUnifiedShare(accountName: String, credentials: ServerCredentials): Boolean { + unifiedShareSupport[accountName]?.let { return it } + + // a failed capability request stays uncached so that the next listing can resolve it again + val supported = runCatching { credentials.supportsUnifiedShare() }.getOrNull() ?: return false + unifiedShareSupport[accountName] = supported + + return supported + } + + private suspend fun ShareAvatarRepository.fetchSharees(file: OCFile) { + file.sharees = fetchShareAvatars(file.localId.toString())?.toAvatarSharees().orEmpty() + } + + private fun List.toAvatarSharees(): List = asSequence() + .flatMap { share -> share.invitedRecipients } + .distinctBy { recipient -> recipient.value } + .map { recipient -> ShareeUser(recipient.value, recipient.displayName, ShareType.USER) } + .toList() +} diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index 85af19137bc7..8245d79b3b63 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -18,6 +18,7 @@ import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.ResultParser; import com.nextcloud.utils.e2ee.E2EVersionHelper; +import com.nextcloud.utils.share.UnifiedShareSharees; import com.nextcloud.utils.extensions.StringExtensionsKt; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; @@ -629,6 +630,8 @@ private void synchronizeData(List folderAndFiles) { updateFileNameForEncryptedFile(fileDataStorageManager, metadata, mLocalFolder); } + UnifiedShareSharees.fillBlocking(user, updatedFiles); + fileDataStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values()); mChildren = updatedFiles; diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java index 57edd26105c3..da3d859ced35 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -15,6 +15,7 @@ import android.text.TextUtils; import com.nextcloud.client.account.User; +import com.nextcloud.utils.share.UnifiedShareSharees; import com.nextcloud.client.jobs.download.FileDownloadHelper; import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorkerNotificationManager; import com.nextcloud.utils.extensions.ExtensionsKt; @@ -349,6 +350,8 @@ private void synchronizeData(List folderAndFiles) throws OperationCancel } // save updated contents in local database + UnifiedShareSharees.fillBlocking(user, updatedFiles); + storageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values()); mLocalFolder.setLastSyncDateForData(System.currentTimeMillis()); storageManager.saveFile(mLocalFolder); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index a027b0e55706..b65ecf532d4e 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -615,12 +615,7 @@ private void bindSharedAvatars(ListItemViewHolder holder, OCFile file) { sharedAvatars.setBoundFileId(fileId); sharedAvatars.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); - avatarShareesProvider.get(file, user, userId, avatars -> { - if (Long.valueOf(fileId).equals(sharedAvatars.getBoundFileId())) { - sharedAvatars.setAvatars(user, avatars, viewThemeUtils); - } - return Unit.INSTANCE; - }); + sharedAvatars.setAvatars(user, avatarShareesProvider.get(file, userId), viewThemeUtils); } private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { @@ -1106,7 +1101,6 @@ public void updateFileIndicator(int iconId, OCFile file) { public void cleanup() { ocFileListDelegate.cleanup(); helper.cleanup(); - avatarShareesProvider.cleanup(); } @SuppressLint("NotifyDataSetChanged") @@ -1132,8 +1126,6 @@ public void removeFile(@NonNull OCFile file) { @SuppressLint("NotifyDataSetChanged") public void updateFile(@NonNull OCFile updatedFile) { - avatarShareesProvider.invalidate(updatedFile); - int allIndex = helper.indexOfSameRemoteFile(mFilesAll, updatedFile); if (allIndex != -1) { mFilesAll.set(allIndex, updatedFile); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt index 37fe382e9934..613c65c25fdb 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt @@ -9,12 +9,6 @@ package com.owncloud.android.ui.adapter.helper import android.content.Context import android.content.res.Resources -import com.nextcloud.android.common.ui.network.auth.ServerCredentials -import com.nextcloud.android.common.ui.share.avatar.ShareAvatarRepository -import com.nextcloud.android.common.ui.share.model.api.share.Share -import com.nextcloud.client.account.User -import com.nextcloud.utils.extensions.supportsUnifiedShare -import com.nextcloud.utils.extensions.toServerCredentials import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager @@ -23,117 +17,25 @@ import com.owncloud.android.lib.resources.shares.ShareeUser import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class AvatarShareesProvider { - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val cache = object : LinkedHashMap>( - CACHE_INITIAL_CAPACITY, - CACHE_LOAD_FACTOR, - true - ) { - override fun removeEldestEntry(eldest: Map.Entry>): Boolean = size > CACHE_MAX_SIZE - } - - private val pendingRequests = mutableMapOf) -> Unit>>() - - private var unifiedShareCredentials: Deferred? = null - - fun invalidate(file: OCFile) { - cache.remove(file.fileId) - } - - fun get(file: OCFile, user: User?, userId: String?, onSharees: (List) -> Unit) { - val fileId = file.fileId - - cache[fileId]?.let { cached -> - onSharees(cached) - return - } - - val localSharees = file.toLocalSharees(userId) - onSharees(localSharees) - - resolveRemoteSharees(file, user, localSharees, onSharees) - } - - private fun resolveRemoteSharees( - file: OCFile, - user: User?, - localSharees: List, - onSharees: (List) -> Unit - ) { - val fileId = file.fileId - - if (user == null) { - cache[fileId] = localSharees - return - } + fun get(file: OCFile, userId: String?): List { + val sharees = file.sharees - pendingRequests[fileId]?.let { waitingCallbacks -> - waitingCallbacks.add(onSharees) - return - } - pendingRequests[fileId] = mutableListOf(onSharees) - - val credentials = unifiedShareCredentials - ?: scope.async { resolveUnifiedShareCredentials(user) }.also { unifiedShareCredentials = it } - - scope.launch { - val sharees = credentials.await()?.let { fetchRemoteSharees(it, file) } ?: localSharees - - withContext(Dispatchers.Main) { - cache[fileId] = sharees - pendingRequests.remove(fileId)?.forEach { callback -> callback(sharees) } - } - } - } - - fun cleanup() { - cache.clear() - pendingRequests.clear() - unifiedShareCredentials = null - scope.coroutineContext.cancelChildren() - } - - private suspend fun resolveUnifiedShareCredentials(user: User): ServerCredentials? = runCatching { - user.toServerCredentials()?.takeIf { it.supportsUnifiedShare() } - }.getOrNull() - - private suspend fun fetchRemoteSharees(credentials: ServerCredentials, file: OCFile): List? = - runCatching { - ShareAvatarRepository(credentials) - .fetchShareAvatars(file.localId.toString()) - ?.toAvatarSharees() - }.getOrNull() - - private fun OCFile.toLocalSharees(userId: String?): List { - val ownerSharee = ownerId + val ownerSharee = file.ownerId ?.takeIf { it.isNotEmpty() && it != userId } - ?.let { ShareeUser(it, ownerDisplayName, ShareType.USER) } + ?.let { ShareeUser(it, file.ownerDisplayName, ShareType.USER) } ?.takeIf { it !in sharees } return listOfNotNull(ownerSharee) + sharees.asReversed() } - private fun List.toAvatarSharees(): List = asSequence() - .flatMap { share -> share.invitedRecipients } - .distinctBy { recipient -> recipient.value } - .map { recipient -> ShareeUser(recipient.value, recipient.displayName, ShareType.USER) } - .toList() - companion object { - private const val CACHE_MAX_SIZE = 500 - private const val CACHE_INITIAL_CAPACITY = 64 - private const val CACHE_LOAD_FACTOR = 0.75f - private val cachedAvatarScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @JvmStatic diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 3e54e20630e9..04a37b8fd439 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -15,6 +15,7 @@ import android.content.ContentValues import androidx.lifecycle.lifecycleScope import com.nextcloud.client.account.User import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.share.UnifiedShareSharees import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile @@ -133,7 +134,7 @@ class OCFileListSearchTask( resultData, storageManager, currentUser.accountName - ) + ).also { UnifiedShareSharees.fill(currentUser, it) } } else { parseAndSaveVirtuals(resultData, fragment) } @@ -216,6 +217,7 @@ class OCFileListSearchTask( var ocFile = FileStorageUtils.fillOCFile(remoteFile) FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, currentUser.accountName) resolveLocalFileId(ocFile) + UnifiedShareSharees.fill(currentUser, listOf(ocFile)) ocFile = storageManager.saveFileWithParent(ocFile, activity) ocFile = handleEncryptionIfNeeded(ocFile, storageManager, activity) { cachedClient ?: currentUser.toPlatformAccount().also { cachedClient = it } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt index 2f9b050622cf..ceeb1c4082fb 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt @@ -16,6 +16,7 @@ import com.nextcloud.client.di.Injectable import com.nextcloud.client.logger.Logger import com.nextcloud.common.SessionTimeOut import com.owncloud.android.R +import com.nextcloud.utils.share.UnifiedShareSharees import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation @@ -88,6 +89,7 @@ class SharedListFragment : parentId = partialFile.parentId } FileStorageUtils.searchForLocalFileInDefaultPath(file, user.accountName) + UnifiedShareSharees.fill(user, listOf(file)) val savedFile = containerActivity.storageManager.saveFileWithParent(file, context) savedFile } else {