diff --git a/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt index 61ea4a09c54a..220d28d2c038 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt @@ -12,6 +12,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import com.nextcloud.client.database.entity.ShareEntity +import com.nextcloud.client.database.entity.model.ShareeKey @Dao interface ShareDao { @@ -21,4 +22,10 @@ interface ShareDao { @Query("DELETE FROM ocshares WHERE owner_share = :accountName") suspend fun clearSharesForAccount(accountName: String) + + @Query( + "SELECT path, shate_with, share_type FROM ocshares " + + "WHERE path IN (:paths) AND owner_share = :accountName AND share_type IN (:shareTypes)" + ) + fun getShareeKeys(paths: List, accountName: String, shareTypes: List): List } diff --git a/app/src/main/java/com/nextcloud/client/database/entity/model/ShareeKey.kt b/app/src/main/java/com/nextcloud/client/database/entity/model/ShareeKey.kt new file mode 100644 index 000000000000..4f7e0b742239 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/entity/model/ShareeKey.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.entity.model + +import androidx.room.ColumnInfo +import com.owncloud.android.lib.resources.shares.ShareType + +data class ShareeKey( + @ColumnInfo(name = "path") val path: String, + @ColumnInfo(name = "shate_with") val shareWith: String?, + @ColumnInfo(name = "share_type") val shareType: Int +) { + companion object { + val shareableShareTypeValues = listOf( + ShareType.USER, + ShareType.GROUP, + ShareType.EMAIL, + ShareType.FEDERATED, + ShareType.FEDERATED_GROUP, + ShareType.ROOM, + ShareType.CIRCLE + ).map { it.value } + } +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index 403736d83333..c40b06e43761 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -7,14 +7,44 @@ package com.nextcloud.utils.extensions +import com.nextcloud.client.database.entity.model.ShareeKey import com.nextcloud.client.database.entity.toOCCapability import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.status.OCCapability import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +private const val SHARE_PATH_QUERY_CHUNK_SIZE = 400 + +fun FileDataStorageManager.areShareesChanged(remoteFiles: List): Boolean { + if (remoteFiles.isEmpty()) { + return false + } + + val newShareesByPath = remoteFiles.asSequence() + .filter { it.remotePath != null } + .groupBy { it.remotePath as String } + .mapValues { (_, files) -> + files.asSequence() + .flatMap { file -> file.sharees.orEmpty().asSequence() } + .mapNotNull { sharee -> sharee.shareType?.let { "${sharee.userId}:${it.value}" } } + .toSet() + } + + val existingShareesByPath = newShareesByPath.keys + .chunked(SHARE_PATH_QUERY_CHUNK_SIZE) + .flatMap { chunk -> shareDao.getShareeKeys(chunk, user.accountName, ShareeKey.shareableShareTypeValues) } + .groupBy(ShareeKey::path) { "${it.shareWith}:${it.shareType}" } + .mapValues { (_, keys) -> keys.toSet() } + + return newShareesByPath.keys.any { path -> + newShareesByPath[path].orEmpty() != existingShareesByPath[path].orEmpty() + } +} + suspend fun FileDataStorageManager.saveShares(shares: List, accountName: String) { withContext(Dispatchers.IO) { val entities = shares.map { share -> diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 0b44558170cd..1db87a82ea63 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -49,6 +49,7 @@ import com.nextcloud.utils.date.DateFormatPattern; import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.nextcloud.utils.extensions.DateExtensionsKt; +import com.nextcloud.utils.extensions.FileDataStorageManagerExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; import com.owncloud.android.MainApp; import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile; @@ -1830,17 +1831,22 @@ public void removeShare(OCShare share) { } } - public void saveSharesFromRemoteFile(List shares) { + /** + * @return true if the sharees of any of the given files differ from what is currently stored locally. + */ + public boolean saveSharesFromRemoteFile(List shares) { if (shares == null || shares.isEmpty()) { - return; + return false; } - // Prepare reset operations Set uniquePaths = new HashSet<>(); for (RemoteFile share : shares) { uniquePaths.add(share.getRemotePath()); } + boolean sharesChanged = FileDataStorageManagerExtensionsKt.areShareesChanged(this, shares); + + // Prepare reset operations ArrayList resetOperations = new ArrayList<>(); for (String path : uniquePaths) { resetShareFlagInAFile(path); @@ -1858,6 +1864,8 @@ public void saveSharesFromRemoteFile(List shares) { if (!insertOperations.isEmpty()) { applyBatch(insertOperations); } + + return sharesChanged; } /** 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 1e76a4d8bc17..3aaab10afab0 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -134,6 +134,11 @@ public class RefreshFolderOperation extends RemoteOperation { */ private boolean mRemoteFolderChanged; + /** + * 'True' means that the sharees of at least one child of the folder changed + */ + private boolean sharesChanged; + /** * 'True' means that Etag will be ignored */ @@ -307,10 +312,10 @@ protected RemoteOperationResult run(OwnCloudClient client) { } } - fileDataStorageManager.saveSharesFromRemoteFile(remoteFiles); + sharesChanged = fileDataStorageManager.saveSharesFromRemoteFile(remoteFiles); } - if (!mSyncFullAccount && mLocalFolder != null && !isMetadataSyncWorkerRunning) { + if (!mSyncFullAccount && sharesChanged && mLocalFolder != null && !isMetadataSyncWorkerRunning) { sendLocalBroadcast(EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 29ba6415519c..2b3957a388e9 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -227,6 +227,8 @@ class FileDisplayActivity : private var mPlayerConnection: PlayerServiceConnection? = null private var lastDisplayedAccountName: String? = null + private var listFragmentJustCreated = false + @Inject lateinit var localBroadcastManager: LocalBroadcastManager @@ -559,6 +561,7 @@ class FileDisplayActivity : val transaction = supportFragmentManager.beginTransaction() transaction.add(R.id.left_fragment_container, listOfFiles, TAG_LIST_OF_FILES) transaction.commit() + listFragmentJustCreated = true } else { supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) } @@ -567,8 +570,11 @@ class FileDisplayActivity : private fun initFragments() { // First fragment val listOfFiles = this.listOfFilesFragment - if (listOfFiles != null && TextUtils.isEmpty(searchQuery)) { - listOfFiles.listDirectory(getCurrentDir(), file, MainApp.isOnlyOnDevice()) + if (listOfFiles != null && searchQuery.isNullOrEmpty()) { + if (!listFragmentJustCreated) { + listOfFiles.listDirectory(getCurrentDir(), file, MainApp.isOnlyOnDevice()) + } + listFragmentJustCreated = true } else { Log_OC.e(TAG, "Still have a chance to lose the initialization of list fragment >(") } @@ -610,10 +616,13 @@ class FileDisplayActivity : if (it::class != OCFileListFragment::class) { leftFragment = OCFileListFragment() supportFragmentManager.executePendingTransactions() + listFragmentJustCreated = true } } - browseToRoot() + // The onResume() that always follows this same-activity intent redelivery already + // lists and re-syncs the current directory, so doing it again here is redundant. + browseToRoot(performRefresh = false) } LIST_GROUPFOLDERS == action -> { @@ -1370,6 +1379,9 @@ class FileDisplayActivity : super.onResume() + val listFragmentJustCreated = this.listFragmentJustCreated + this.listFragmentJustCreated = false + folderRefreshScheduler.start() if (ocFileListFragment?.isSearchFragment == true) { @@ -1410,7 +1422,9 @@ class FileDisplayActivity : if (searchView != null && !TextUtils.isEmpty(searchQuery)) { searchView?.setQuery(searchQuery, false) } else if (!ocFileListFragment.isSearchFragment && startFile == null) { - ocFileListFragment.listDirectory(MainApp.isOnlyOnDevice()) + if (!listFragmentJustCreated) { + ocFileListFragment.listDirectory(MainApp.isOnlyOnDevice()) + } ocFileListFragment.registerFabListener() updateActionBarTitleAndHomeButton(currentDir) } else { @@ -1553,6 +1567,9 @@ class FileDisplayActivity : return } + // EVENT_SINGLE_FOLDER_CONTENTS_SYNCED fires only when the folder's content actually changed, and + // EVENT_SINGLE_FOLDER_SHARES_SYNCED only when a sharee actually changed - each is an independent, + // already-precise signal, so both are handled here (RefreshFolderOperation.java). var currentFile = file?.remotePath?.let { storageManager.getFileByPath(it) } val currentDir = getCurrentDir()?.remotePath?.let { storageManager.getFileByPath(it) } val isSyncFolderRemotePathRoot = OCFile.ROOT_PATH == syncFolderRemotePath @@ -1914,13 +1931,15 @@ class FileDisplayActivity : } // endregion - fun browseToRoot() { + fun browseToRoot(performRefresh: Boolean = true) { listOfFilesFragment?.let { val root = storageManager.getFileByPath(OCFile.ROOT_PATH) it.resetSearchAttributes() file = root - it.listDirectory(root, MainApp.isOnlyOnDevice()) - startSyncFolderOperation(root, false) + if (performRefresh) { + it.listDirectory(root, MainApp.isOnlyOnDevice()) + startSyncFolderOperation(root, false) + } } binding.fabMain.setImageResource(R.drawable.ic_plus) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index b0b5c69e0b7a..aed634b43167 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -570,6 +570,9 @@ open class FolderPickerActivity : } if (FileSyncAdapter.EVENT_FULL_SYNC_START != event) { + // EVENT_SINGLE_FOLDER_CONTENTS_SYNCED fires only when the folder's content actually + // changed, and EVENT_SINGLE_FOLDER_SHARES_SYNCED only when a sharee actually changed - + // each is an independent, already-precise signal (RefreshFolderOperation.java). var (currentFile, currentDir) = getCurrentFileAndDirectory() if (currentDir == null) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 1ce195ff29b8..4b470b74a7b5 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -1604,6 +1604,8 @@ public void listDirectory(@Nullable OCFile directory, OCFile file, boolean onlyO return; } + Log_OC.i(TAG, "listing directory: " + directory.getDecryptedRemotePath()); + mAdapter.swapDirectory( accountManager.getUser(), directory, @@ -1616,14 +1618,15 @@ public void listDirectory(@Nullable OCFile directory, OCFile file, boolean onlyO updateLayout(); + final var recyclerView = getRecyclerView(); if (file != null) { mAdapter.setHighlightedItem(file); int position = mAdapter.getItemPosition(file); - if (position != -1 && getRecyclerView() != null) { - getRecyclerView().scrollToPosition(position); + if (position != -1 && recyclerView != null) { + recyclerView.scrollToPosition(position); } - } else if (getRecyclerView() != null && (previousDirectory == null || !previousDirectory.equals(directory))) { - getRecyclerView().scrollToPosition(0); + } else if (recyclerView != null && (previousDirectory == null || !previousDirectory.equals(directory))) { + recyclerView.scrollToPosition(0); } } else if (isSearchEventSet(searchEvent)) { handleSearchEvent(searchEvent);