diff --git a/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt index ddb35ff46a5b..e72ef28b2376 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt @@ -11,6 +11,7 @@ import androidx.room.Dao import androidx.room.Query import com.nextcloud.client.database.entity.SyncedFolderEntity import com.owncloud.android.db.ProviderMeta +import kotlinx.coroutines.flow.Flow @Dao interface SyncedFolderDao { @@ -23,4 +24,7 @@ interface SyncedFolderDao { """ ) fun findByLocalPathAndAccount(localPath: String, account: String): SyncedFolderEntity? + + @Query("SELECT * FROM ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME}") + fun getAllAsFlow(): Flow> } diff --git a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt index e2ad9e5d4dc5..f614e50aa4ba 100644 --- a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt +++ b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt @@ -24,6 +24,7 @@ import androidx.core.graphics.drawable.toDrawable import com.nextcloud.client.account.User import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolderObserver import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.ui.activity.FileActivity @@ -91,7 +92,7 @@ class ShortcutUtil @Inject constructor(private val mContext: Context) { thumbnail != null -> IconCompat.createWithAdaptiveBitmap(bitmapToAdaptiveBitmap(thumbnail)) file.isFolder -> { - val isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user) + val isAutoUploadFolder = SyncedFolderObserver.isAutoUploadFolder(file, user) val isDarkModeActive = syncedFolderProvider.preferences.isDarkModeEnabled val overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder) val drawable = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, mContext, viewThemeUtils) diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderObserver.kt b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderObserver.kt new file mode 100644 index 000000000000..fbb4779fec0f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderObserver.kt @@ -0,0 +1,54 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.datamodel + +import com.nextcloud.client.account.User +import com.nextcloud.client.database.dao.SyncedFolderDao +import com.owncloud.android.lib.resources.files.model.ServerFileInterface +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +object SyncedFolderObserver { + + @Volatile + private var syncedFoldersMap = mapOf>() + + private var job: Job? = null + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + fun start(dao: SyncedFolderDao) { + if (job?.isActive == true) return + + job = scope.launch { + dao.getAllAsFlow() + .distinctUntilChanged() + .collect { updatedEntities -> + syncedFoldersMap = updatedEntities + .filter { it.remotePath != null && it.account != null } + .groupBy { it.account!! } + .mapValues { (_, entities) -> + entities.map { it.remotePath!!.trimEnd('/') }.toSet() + } + } + } + } + + @Suppress("ReturnCount") + fun isAutoUploadFolder(file: ServerFileInterface, user: User): Boolean { + val accountFolders = syncedFoldersMap[user.accountName] ?: return false + val normalizedRemotePath = file.remotePath.trimEnd('/') + if (normalizedRemotePath.isEmpty()) return false + return accountFolders.any { entityPath -> + normalizedRemotePath == entityPath + } + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java index a2fb5cb0afb4..6eb1b69c2c1e 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java @@ -25,7 +25,6 @@ import com.owncloud.android.MainApp; import com.owncloud.android.db.ProviderMeta; import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.model.ServerFileInterface; import java.io.File; import java.util.ArrayList; @@ -61,6 +60,7 @@ public SyncedFolderProvider(ContentResolver contentResolver, AppPreferences pref mContentResolver = contentResolver; this.preferences = preferences; this.clock = clock; + SyncedFolderObserver.INSTANCE.start(dao); } /** @@ -84,23 +84,19 @@ public long storeSyncedFolder(SyncedFolder syncedFolder) { } } - public static boolean isAutoUploadFolder(SyncedFolderProvider syncedFolderProvider, ServerFileInterface file, User user) { - return syncedFolderProvider != null && syncedFolderProvider.findByRemotePathAndAccount(file.getRemotePath(), user); - } - public int countEnabledSyncedFolders() { int count = 0; Cursor cursor = mContentResolver.query( - ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, - null, - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED + " = ?", - new String[]{"1"}, - null - ); + ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, + null, + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED + " = ?", + new String[]{"1"}, + null + ); if (cursor != null) { - count = cursor.getCount(); - cursor.close(); + count = cursor.getCount(); + cursor.close(); } return count; @@ -154,12 +150,12 @@ public int updateSyncedFolderEnabled(long id, Boolean enabled) { int result = 0; Cursor cursor = mContentResolver.query( - ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, - null, - ProviderMeta.ProviderTableMeta._ID + "=?", - new String[]{String.valueOf(id)}, - null - ); + ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, + null, + ProviderMeta.ProviderTableMeta._ID + "=?", + new String[]{String.valueOf(id)}, + null + ); if (cursor != null && cursor.getCount() == 1) { while (cursor.moveToNext()) { @@ -177,7 +173,7 @@ public int updateSyncedFolderEnabled(long id, Boolean enabled) { Log_OC.e(TAG, "Sync folder db cursor for ID=" + id + " in NULL."); } else { Log_OC.e(TAG, cursor.getCount() + " items for id=" + id + " available in sync folder database. " + - "Expected 1. Failed to update sync folder db."); + "Expected 1. Failed to update sync folder db."); } } @@ -226,10 +222,10 @@ public SyncedFolder getSyncedFolderByID(Long syncedFolderID) { */ public int deleteSyncFoldersForAccount(User user) { return mContentResolver.delete( - ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + " = ?", - new String[]{String.valueOf(user.getAccountName())} - ); + ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + " = ?", + new String[]{String.valueOf(user.getAccountName())} + ); } /** @@ -284,10 +280,10 @@ public void updateAutoUploadPaths(Context context) { */ public int deleteSyncedFoldersNotInList(List ids) { int result = mContentResolver.delete( - ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, - ProviderMeta.ProviderTableMeta._ID + " NOT IN (?)", - new String[]{String.valueOf(ids)} - ); + ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, + ProviderMeta.ProviderTableMeta._ID + " NOT IN (?)", + new String[]{String.valueOf(ids)} + ); if(result > 0) { preferences.setLegacyClean(true); @@ -301,10 +297,10 @@ public int deleteSyncedFoldersNotInList(List ids) { */ public int deleteSyncedFolder(long id) { return mContentResolver.delete( - ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, - ProviderMeta.ProviderTableMeta._ID + " = ?", - new String[]{String.valueOf(id)} - ); + ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, + ProviderMeta.ProviderTableMeta._ID + " = ?", + new String[]{String.valueOf(id)} + ); } public AppPreferences getPreferences() { @@ -323,11 +319,11 @@ public int updateSyncFolder(SyncedFolder syncedFolder) { ContentValues cv = createContentValuesFromSyncedFolder(syncedFolder); return mContentResolver.update( - ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, - cv, - ProviderMeta.ProviderTableMeta._ID + "=?", - new String[]{String.valueOf(syncedFolder.getId())} - ); + ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, + cv, + ProviderMeta.ProviderTableMeta._ID + "=?", + new String[]{String.valueOf(syncedFolder.getId())} + ); } /** @@ -341,33 +337,33 @@ private SyncedFolder createSyncedFolderFromCursor(Cursor cursor) { if (cursor != null) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta._ID)); String localPath = cursor.getString(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH)); + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH)); String remotePath = cursor.getString(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH)); + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH)); boolean wifiOnly = cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY)) == 1; + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY)) == 1; boolean chargingOnly = cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY)) == 1; + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY)) == 1; boolean existing = cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXISTING)) == 1; + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXISTING)) == 1; boolean subfolderByDate = cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE)) == 1; + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE)) == 1; String accountName = cursor.getString(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT)); + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT)); int uploadAction = cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION)); + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION)); int nameCollisionPolicy = cursor.getInt(cursor.getColumnIndexOrThrow( ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_NAME_COLLISION_POLICY)); boolean enabled = cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED)) == 1; + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED)) == 1; long enabledTimestampMs = cursor.getLong(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED_TIMESTAMP_MS)); + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED_TIMESTAMP_MS)); MediaFolderType type = MediaFolderType.getById(cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_TYPE))); + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_TYPE))); boolean hidden = cursor.getInt(cursor.getColumnIndexOrThrow( ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_HIDDEN)) == 1; SubFolderRule subFolderRule = SubFolderRule.values()[cursor.getInt( - cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_RULE))]; + cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_RULE))]; boolean excludeHidden = cursor.getInt(cursor.getColumnIndexOrThrow( ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXCLUDE_HIDDEN)) == 1; long lastScanTimestampMs = cursor.getLong(cursor.getColumnIndexOrThrow( @@ -423,48 +419,4 @@ private ContentValues createContentValuesFromSyncedFolder(SyncedFolder syncedFol cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LAST_SCAN_TIMESTAMP_MS, syncedFolder.getLastScanTimestampMs()); return cv; } - - /** - * method to check if sync folder for the remote path exist in table or not - * - * @param remotePath to be check - * @param user for which we are looking - * @return true if exist, false otherwise - */ - public boolean findByRemotePathAndAccount(String remotePath, User user) { - if (user == null) { - return false; - } - - boolean result = false; - - //if path ends with / then remove the last / to work the query right way - //because the sub folders of synced folders will not have the slash at the end - if (remotePath.endsWith("/")) { - remotePath = remotePath.substring(0, remotePath.length() - 1); - } - - Cursor cursor = mContentResolver.query( - ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, - null, - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH + " LIKE ? AND " + - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + " =? ", - new String[]{"%" + remotePath + "%", user.getAccountName()}, - null); - - if (cursor != null && cursor.getCount() >= 1) { - result = true; - } else { - if (cursor == null) { - Log_OC.e(TAG, "Sync folder db cursor for remote path = " + remotePath + " in NULL."); - } - } - - if (cursor != null) { - cursor.close(); - } - - return result; - - } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java index cb03db76006b..fb17f5a5bc7a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java @@ -31,6 +31,7 @@ import com.owncloud.android.R; import com.owncloud.android.databinding.RichdocumentsWebviewBinding; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.datamodel.SyncedFolderObserver; import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.ui.asynctasks.TextEditorLoadUrlTask; @@ -251,7 +252,7 @@ protected void setThumbnailView(final User user) { // Todo minimize: only icon by mimetype OCFile file = getFile(); if (file.isFolder()) { - boolean isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user); + boolean isAutoUploadFolder = SyncedFolderObserver.INSTANCE.isAutoUploadFolder(file, user); Integer overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder); LayerDrawable drawable = MimeTypeUtil.getFolderIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java index 3c6b724e553f..b7eab1152adc 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java @@ -21,6 +21,7 @@ import com.owncloud.android.R; import com.owncloud.android.databinding.ShareActivityBinding; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.datamodel.SyncedFolderObserver; import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.lib.common.operations.RemoteOperation; @@ -63,14 +64,14 @@ protected void onCreate(Bundle savedInstanceState) { OCFile file = getFile(); Optional optionalUser = getUser(); - if (!optionalUser.isPresent()) { + if (optionalUser.isEmpty()) { finish(); return; } // Icon if (file.isFolder()) { - boolean isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, optionalUser.get()); + boolean isAutoUploadFolder = SyncedFolderObserver.INSTANCE.isAutoUploadFolder(file, optionalUser.get()); Integer overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder); LayerDrawable drawable = MimeTypeUtil.getFolderIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt index a6b48b20c60e..cbac02172939 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt @@ -18,6 +18,7 @@ import com.nextcloud.client.account.User import com.owncloud.android.databinding.UploaderListItemLayoutBinding import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolderObserver import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncThumbnailDrawable @@ -113,7 +114,7 @@ class ReceiveExternalFilesAdapter( } private fun setupThumbnailForFolder(thumbnailImageView: ImageView, file: OCFile) { - val isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user) + val isAutoUploadFolder = SyncedFolderObserver.isAutoUploadFolder(file, user) val isDarkModeActive = syncedFolderProvider.preferences.isDarkModeEnabled val overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder) val icon = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils) diff --git a/app/src/main/java/com/owncloud/android/utils/overlay/OverlayManager.kt b/app/src/main/java/com/owncloud/android/utils/overlay/OverlayManager.kt index f1300d9805be..36feeb108594 100644 --- a/app/src/main/java/com/owncloud/android/utils/overlay/OverlayManager.kt +++ b/app/src/main/java/com/owncloud/android/utils/overlay/OverlayManager.kt @@ -16,6 +16,7 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolderObserver import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.MimeTypeUtil @@ -34,8 +35,7 @@ class OverlayManager @Inject constructor( val overlayIconId = folder ?.takeIf { it.isFolder } ?.let { currentFolder -> - val isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder( - syncedFolderProvider, + val isAutoUploadFolder = SyncedFolderObserver.isAutoUploadFolder( currentFolder, accountManager.user ) @@ -56,7 +56,7 @@ class OverlayManager @Inject constructor( DisplayUtils.stopShimmer(loaderImageView, imageView) val isAutoUploadFolder = - SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, folder, accountManager.user) + SyncedFolderObserver.isAutoUploadFolder(folder, accountManager.user) val isDarkModeActive = preferences.isDarkModeEnabled() val overlayIconId = folder.getFileOverlayIconId(isAutoUploadFolder)