From 7c3b780be23966537b236b51d431631cc4555f0c Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 12:10:03 +0100 Subject: [PATCH 01/12] fix: file system provider Signed-off-by: alperozturk --- .../client/database/dao/FileSystemDao.kt | 16 ++ .../client/jobs/BackgroundJobFactory.kt | 2 +- .../jobs/autoUpload/AutoUploadHelper.kt | 9 +- .../jobs/autoUpload/AutoUploadWorker.kt | 6 +- .../jobs/autoUpload/FileSystemRepository.kt | 78 +++++++++- .../android/datamodel/FileSystemDataSet.java | 108 -------------- .../datamodel/FilesystemDataProvider.java | 139 ------------------ .../android/utils/FilesSyncHelper.java | 112 ++------------ 8 files changed, 108 insertions(+), 362 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt index fcb567abc210..3dc90d77b318 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt @@ -8,12 +8,17 @@ package com.nextcloud.client.database.dao import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import com.nextcloud.client.database.entity.FilesystemEntity import com.owncloud.android.db.ProviderMeta @Dao interface FileSystemDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(filesystemEntity: FilesystemEntity) + @Query( """ SELECT * @@ -37,4 +42,15 @@ interface FileSystemDao { """ ) suspend fun markFileAsUploaded(localPath: String, syncedFolderId: String) + + @Query( + """ + SELECT * + FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} + WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH} = :localPath + AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId + LIMIT 1 + """ + ) + fun getFileByPathAndFolder(localPath: String, syncedFolderId: String): FilesystemEntity? } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index b7d06ee2d9d3..3e67df51fa92 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -179,7 +179,7 @@ class BackgroundJobFactory @Inject constructor( powerManagementService = powerManagementService, syncedFolderProvider = syncedFolderProvider, backgroundJobManager = backgroundJobManager.get(), - repository = FileSystemRepository(dao = database.fileSystemDao()), + repository = FileSystemRepository(dao = database.fileSystemDao(), context), viewThemeUtils = viewThemeUtils.get() ) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt index a43b524b9d16..9250d65ce3bd 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt @@ -29,7 +29,7 @@ class AutoUploadHelper { private const val MAX_DEPTH = 100 } - fun insertCustomFolderIntoDB(folder: SyncedFolder, filesystemDataProvider: FilesystemDataProvider?): Int { + fun insertCustomFolderIntoDB(folder: SyncedFolder, repository: FileSystemRepository?): Int { val path = Paths.get(folder.localPath) if (!Files.exists(path)) { @@ -78,12 +78,7 @@ class AutoUploadHelper { val localPath = file.toLocalPath() - filesystemDataProvider?.storeOrUpdateFileValue( - localPath, - lastModified, - javaFile.isDirectory, - folder - ) + repository?.insertOrReplace(localPath,lastModified, folder) fileCount++ diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index 5a793e5f8191..8ae9d481af2d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -230,16 +230,16 @@ class AutoUploadWorker( private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array?) = try { withContext(Dispatchers.IO) { if (contentUris.isNullOrEmpty()) { - FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper) + FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper, repository) } else { - val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris) + val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris, repository) if (!isContentUrisStored) { Log_OC.w( TAG, "changed content uris not stored, fallback to insert all db entries to not lose files" ) - FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper) + FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper, repository) } } syncedFolder.lastScanTimestampMs = System.currentTimeMillis() diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index a4bf50a03fa9..fbee2fedaab3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -7,13 +7,18 @@ package com.nextcloud.client.jobs.autoUpload +import android.content.Context +import android.net.Uri +import android.provider.MediaStore import com.nextcloud.client.database.dao.FileSystemDao +import com.nextcloud.client.database.entity.FilesystemEntity import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.SyncedFolderUtils import java.io.File +import java.util.zip.CRC32 -class FileSystemRepository(private val dao: FileSystemDao) { +class FileSystemRepository(private val dao: FileSystemDao, private val context: Context) { companion object { private const val TAG = "FilesystemRepository" @@ -63,4 +68,75 @@ class FileSystemRepository(private val dao: FileSystemDao) { Log_OC.e(TAG, "Error marking file as uploaded: ${e.message}", e) } } + + fun insert(uri: Uri, syncedFolder: SyncedFolder) { + val projection = arrayOf(MediaStore.MediaColumns.DATA) + val cursor = context.contentResolver.query(uri, projection, null, null, null) + + cursor?.use { + val columnIndexData = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) + + while (cursor.moveToNext()) { + val filePath = cursor.getString(columnIndexData) + Log_OC.d(TAG, "attempt to insert new file entity uri: $uri") + insertOrReplace(filePath, null, syncedFolder) + } + } ?: Log_OC.w(TAG, "Cursor is null for URI: $uri") + } + + fun insertOrReplace(localPath: String?, lastModified: Long?, syncedFolder: SyncedFolder) { + try { + if (localPath == null) { + Log_OC.w(TAG, "localPath path not exists: $localPath") + return + } + + val file = File(localPath) + if (!file.exists()) { + Log_OC.w(TAG, "local file does not exist, cannot insert or replace: $localPath") + return + } + + val entity = dao.getFileByPathAndFolder(localPath, syncedFolder.id.toString()) + if (entity != null && entity.fileSentForUpload == 1) { + Log_OC.w(TAG, "file already uploaded path: $localPath, syncedFolder: ${syncedFolder.localPath}, ${syncedFolder.id}") + return + } + + val crc = getFileChecksum(file) + + val newEntity = FilesystemEntity( + id = entity?.id, + localPath = localPath, + fileIsFolder = if (file.isDirectory) 1 else 0, + fileFoundRecently = System.currentTimeMillis(), + fileSentForUpload = 0, + syncedFolderId = syncedFolder.id.toString(), + crc32 = crc?.toString(), + fileModified = lastModified ?: file.lastModified() + ) + + Log_OC.d(TAG, "inserting new file system entity: $newEntity") + + dao.insertOrReplace(newEntity) + } catch (e: Exception) { + Log_OC.e(TAG, "Failed to insert/update file: $localPath", e) + } + } + + private fun getFileChecksum(file: File): Long? { + return try { + file.inputStream().use { fis -> + val crc = CRC32() + val buffer = ByteArray(64 * 1024) + var bytesRead: Int + while (fis.read(buffer).also { bytesRead = it } > 0) { + crc.update(buffer, 0, bytesRead) + } + crc.value + } + } catch (_: Exception) { + null + } + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java b/app/src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java deleted file mode 100644 index 44f768c2d511..000000000000 --- a/app/src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Mario Danic - * @author Andy Scherzinger - * Copyright (C) 2017 Mario Danic - * Copyright (C) 2017 Nextcloud - * Copyright (C) 2018 Andy Scherzinger - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.datamodel; - -import androidx.annotation.Nullable; - -/** - * Model for filesystem data from the database. - */ -public class FileSystemDataSet { - private int id; - private String localPath; - private long modifiedAt; - private boolean folder; - private boolean sentForUpload; - private long foundAt; - private long syncedFolderId; - @Nullable private String crc32; - - public FileSystemDataSet(int id, String localPath, long modifiedAt, boolean folder, boolean sentForUpload, long foundAt, long syncedFolderId, String crc32) { - this.id = id; - this.localPath = localPath; - this.modifiedAt = modifiedAt; - this.folder = folder; - this.sentForUpload = sentForUpload; - this.foundAt = foundAt; - this.syncedFolderId = syncedFolderId; - this.crc32 = crc32; - } - - public FileSystemDataSet() { - // empty constructor - } - - public int getId() { - return this.id; - } - - public String getLocalPath() { - return this.localPath; - } - - public long getModifiedAt() { - return this.modifiedAt; - } - - public boolean isFolder() { - return this.folder; - } - - public boolean isSentForUpload() { - return this.sentForUpload; - } - - public long getFoundAt() { - return this.foundAt; - } - - public long getSyncedFolderId() { - return this.syncedFolderId; - } - - @Nullable - public String getCrc32() { - return this.crc32; - } - - public void setId(int id) { - this.id = id; - } - - public void setLocalPath(String localPath) { - this.localPath = localPath; - } - - public void setModifiedAt(long modifiedAt) { - this.modifiedAt = modifiedAt; - } - - public void setFolder(boolean folder) { - this.folder = folder; - } - - public void setSentForUpload(boolean sentForUpload) { - this.sentForUpload = sentForUpload; - } - - public void setFoundAt(long foundAt) { - this.foundAt = foundAt; - } - - public void setSyncedFolderId(long syncedFolderId) { - this.syncedFolderId = syncedFolderId; - } - - public void setCrc32(@Nullable String crc32) { - this.crc32 = crc32; - } -} diff --git a/app/src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java b/app/src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java index e1a2016715f6..48f212e8380e 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java @@ -8,19 +8,10 @@ package com.owncloud.android.datamodel; import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; import com.owncloud.android.db.ProviderMeta; import com.owncloud.android.lib.common.utils.Log_OC; -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.CRC32; - /** * Provider for stored filesystem data. */ @@ -46,134 +37,4 @@ public int deleteAllEntriesForSyncedFolder(String syncedFolderId) { ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ?", new String[]{syncedFolderId}); } - - public void storeOrUpdateFileValue(String localPath, long modifiedAt, boolean isFolder, SyncedFolder syncedFolder) { - Log_OC.d(TAG, "storeOrUpdateFileValue called, localPath: " + localPath + " ID: " + syncedFolder.getId()); - - // takes multiple milliseconds to query data from database (around 75% of execution time) (6ms) - FileSystemDataSet data = getFilesystemDataSet(localPath, syncedFolder); - - int isFolderValue = 0; - if (isFolder) { - isFolderValue = 1; - } - - ContentValues cv = new ContentValues(); - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY, System.currentTimeMillis()); - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_MODIFIED, modifiedAt); - - if (data == null) { - Log_OC.d(TAG, "storeOrUpdateFileValue data is null"); - - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH, localPath); - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER, isFolderValue); - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, Boolean.FALSE); - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID, syncedFolder.getId()); - - long newCrc32 = getFileChecksum(localPath); - if (newCrc32 != -1) { - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32, Long.toString(newCrc32)); - } - - Uri result = contentResolver.insert(ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, cv); - - if (result == null) { - Log_OC.e(TAG, "Failed to insert filesystem data with local path: " + localPath); - } - } else { - Log_OC.d(TAG, "storeOrUpdateFileValue data is not null"); - - if (data.getModifiedAt() != modifiedAt) { - long newCrc32 = getFileChecksum(localPath); - if (data.getCrc32() == null || (newCrc32 != -1 && !data.getCrc32().equals(Long.toString(newCrc32)))) { - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32, Long.toString(newCrc32)); - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, 0); - } - } - - // updating data takes multiple milliseconds (around 25% of exec time) (2 ms) - int result = contentResolver.update( - ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, - cv, - ProviderMeta.ProviderTableMeta._ID + "=?", - new String[]{String.valueOf(data.getId())} - ); - - if (result == 0) { - Log_OC.e(TAG, "Failed to update filesystem data with local path: " + localPath); - } - } - } - - private FileSystemDataSet getFilesystemDataSet(String localPathParam, SyncedFolder syncedFolder) { - Log_OC.d(TAG, "getFilesForUpload called, localPath: " + localPathParam + " ID: " + syncedFolder.getId()); - - String[] projection = { - ProviderMeta.ProviderTableMeta._ID, - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH, - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_MODIFIED, - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER, - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY, - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, - ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32 - }; - - String selection = ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " = ? AND " + - ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ?"; - String[] selectionArgs = { localPathParam, String.valueOf(syncedFolder.getId()) }; - - try (Cursor cursor = contentResolver.query( - ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, - projection, - selection, - selectionArgs, - null - )) { - if (cursor != null && cursor.moveToFirst()) { - int id = cursor.getInt(cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta._ID)); - if (id == -1) { - Log_OC.e(TAG, "Arbitrary value could not be created from cursor"); - return null; - } - - String localPath = cursor.getString(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH)); - long modifiedAt = cursor.getLong(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_MODIFIED)); - boolean isFolder = cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER)) != 0; - long foundAt = cursor.getLong(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY)); - boolean isSentForUpload = cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD)) != 0; - String crc32 = cursor.getString(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32)); - - return new FileSystemDataSet(id, localPath, modifiedAt, isFolder, isSentForUpload, foundAt, - syncedFolder.getId(), crc32); - } - } catch (Exception e) { - Log_OC.e(TAG, "DB error restoring arbitrary values.", e); - } - - return null; - } - - private long getFileChecksum(String filepath) { - - try (FileInputStream fileInputStream = new FileInputStream(filepath); - InputStream inputStream = new BufferedInputStream(fileInputStream)) { - CRC32 crc = new CRC32(); - byte[] buf = new byte[1024 * 64]; - int size; - while ((size = inputStream.read(buf)) > 0) { - crc.update(buf, 0, size); - } - - return crc.getValue(); - - } catch (IOException e) { - return -1; - } - } } diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java index 94461e00d5ad..6d29d58f5b2a 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java @@ -9,9 +9,7 @@ */ package com.owncloud.android.utils; -import android.content.ContentResolver; import android.content.Context; -import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; @@ -20,11 +18,11 @@ import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.jobs.ContentObserverWork; import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper; +import com.nextcloud.client.jobs.autoUpload.FileSystemRepository; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.utils.extensions.UriExtensionsKt; import com.owncloud.android.MainApp; -import com.owncloud.android.datamodel.FilesystemDataProvider; import com.owncloud.android.datamodel.MediaFolderType; import com.owncloud.android.datamodel.SyncedFolder; import com.owncloud.android.datamodel.SyncedFolderProvider; @@ -33,8 +31,6 @@ import java.io.File; -import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR; - /** * Various utilities that make auto upload tick */ @@ -47,44 +43,21 @@ private FilesSyncHelper() { // utility class -> private constructor } - public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, AutoUploadHelper helper) { - Log_OC.d(TAG, "insertAllDBEntriesForSyncedFolder, called. ID: " + syncedFolder.getId()); - - final Context context = MainApp.getAppContext(); - final ContentResolver contentResolver = context.getContentResolver(); - + public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, AutoUploadHelper helper, FileSystemRepository repository) { final long enabledTimestampMs = syncedFolder.getEnabledTimestampMs(); if (syncedFolder.isEnabled() && (syncedFolder.isExisting() || enabledTimestampMs >= 0)) { MediaFolderType mediaType = syncedFolder.getType(); - final long lastCheckTimestampMs = syncedFolder.getLastScanTimestampMs(); - - Log_OC.d(TAG,"File-sync start check folder "+syncedFolder.getLocalPath()); - long startTime = System.nanoTime(); if (mediaType == MediaFolderType.IMAGE) { - Log_OC.d(TAG, "inserting IMAGE"); - FilesSyncHelper.insertContentIntoDB(MediaStore.Images.Media.INTERNAL_CONTENT_URI, - syncedFolder, - lastCheckTimestampMs); - FilesSyncHelper.insertContentIntoDB(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - syncedFolder, - lastCheckTimestampMs); + repository.insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, syncedFolder); + repository.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, syncedFolder); } else if (mediaType == MediaFolderType.VIDEO) { - Log_OC.d(TAG, "inserting VIDEO"); - FilesSyncHelper.insertContentIntoDB(MediaStore.Video.Media.INTERNAL_CONTENT_URI, - syncedFolder, - lastCheckTimestampMs); - FilesSyncHelper.insertContentIntoDB(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, - syncedFolder, - lastCheckTimestampMs); + repository.insert(MediaStore.Video.Media.INTERNAL_CONTENT_URI, syncedFolder); + repository.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, syncedFolder); } else { - Log_OC.d(TAG, "inserting other media types: " + mediaType.toString()); - FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver); - helper.insertCustomFolderIntoDB(syncedFolder, filesystemDataProvider); + helper.insertCustomFolderIntoDB(syncedFolder, repository); } - - Log_OC.d(TAG,"File-sync finished full check for custom folder "+syncedFolder.getLocalPath()+" within "+(System.nanoTime() - startTime)+ "ns"); } else { if (!syncedFolder.isEnabled()) { Log_OC.w(TAG, "insertAllDBEntriesForSyncedFolder, syncedFolder not enabled"); @@ -108,11 +81,8 @@ public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, * @param contentUris An array of content URI strings collected from {@link ContentObserverWork##checkAndTriggerAutoUpload()}. * @return {@code true} if all changed content URIs were successfully stored; {@code false} otherwise. */ - public static boolean insertChangedEntries(SyncedFolder syncedFolder, String[] contentUris) { - Log_OC.d(TAG, "insertChangedEntries, syncedFolderID: " + syncedFolder.getId()); + public static boolean insertChangedEntries(SyncedFolder syncedFolder, String[] contentUris, FileSystemRepository repository) { final Context context = MainApp.getAppContext(); - final ContentResolver contentResolver = context.getContentResolver(); - final FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver); for (String contentUriString : contentUris) { if (contentUriString == null) { Log_OC.w(TAG, "null content uri string"); @@ -144,7 +114,7 @@ public static boolean insertChangedEntries(SyncedFolder syncedFolder, String[] c return false; } - filesystemDataProvider.storeOrUpdateFileValue(filePath, file.lastModified(), file.isDirectory(), syncedFolder); + repository.insertOrReplace(filePath, file.lastModified(), syncedFolder); } Log_OC.d(TAG, "changed content uris successfully stored"); @@ -152,70 +122,6 @@ public static boolean insertChangedEntries(SyncedFolder syncedFolder, String[] c return true; } - private static void insertContentIntoDB(Uri uri, SyncedFolder syncedFolder, - long lastCheckTimestampMs) { - Log_OC.d(TAG, "insertContentIntoDB, URI: " + uri + " syncedFolderID: " + syncedFolder.getId() + " lastCheckTimestampMs " + lastCheckTimestampMs); - final Context context = MainApp.getAppContext(); - final ContentResolver contentResolver = context.getContentResolver(); - - Cursor cursor; - int column_index_data; - int column_index_date_modified; - - final FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver); - - String contentPath; - boolean isFolder; - - String[] projection = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DATE_MODIFIED}; - - String path = syncedFolder.getLocalPath(); - if (!path.endsWith(PATH_SEPARATOR)) { - Log_OC.w(TAG, "path is not ending with: " + PATH_SEPARATOR); - path = path + PATH_SEPARATOR; - } - path = path + "%"; - - long enabledTimestampMs = syncedFolder.getEnabledTimestampMs(); - - cursor = context.getContentResolver().query(uri, projection, MediaStore.MediaColumns.DATA + " LIKE ?", - new String[]{path}, null); - - if (cursor != null) { - column_index_data = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); - column_index_date_modified = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED); - while (cursor.moveToNext()) { - contentPath = cursor.getString(column_index_data); - isFolder = new File(contentPath).isDirectory(); - - if (syncedFolder.getLastScanTimestampMs() != SyncedFolder.NOT_SCANNED_YET && - cursor.getLong(column_index_date_modified) < (lastCheckTimestampMs / 1000)) { - Log_OC.w(TAG, "skipping contentPath"); - continue; - } - - if (syncedFolder.isExisting() || cursor.getLong(column_index_date_modified) >= enabledTimestampMs / 1000) { - // storeOrUpdateFileValue takes a few ms - // -> Rest of this file check takes not even 1 ms. - filesystemDataProvider.storeOrUpdateFileValue(contentPath, - cursor.getLong(column_index_date_modified), isFolder, - syncedFolder); - } else { - if (!syncedFolder.isExisting()) { - Log_OC.w(TAG, "syncedFolder not exists"); - } - - if (cursor.getLong(column_index_date_modified) < enabledTimestampMs / 1000) { - Log_OC.w(TAG, "column_index_date_modified not meeting condition"); - } - } - } - cursor.close(); - } else { - Log_OC.w(TAG, "cursor is null "); - } - } - public static void restartUploadsIfNeeded(final UploadsStorageManager uploadsStorageManager, final UserAccountManager accountManager, final ConnectivityService connectivityService, From 4d40b9038a00022b34ddb96d2be98973dabfa45a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 12:29:26 +0100 Subject: [PATCH 02/12] fix: file system provider Signed-off-by: alperozturk --- .../jobs/autoUpload/AutoUploadHelper.kt | 2 +- .../jobs/autoUpload/FileSystemRepository.kt | 61 +++++++++++++------ 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt index 9250d65ce3bd..febbfe383005 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt @@ -78,7 +78,7 @@ class AutoUploadHelper { val localPath = file.toLocalPath() - repository?.insertOrReplace(localPath,lastModified, folder) + repository?.insertOrReplace(localPath, lastModified, folder) fileCount++ diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index fbee2fedaab3..1a9438d14034 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -18,6 +18,7 @@ import com.owncloud.android.utils.SyncedFolderUtils import java.io.File import java.util.zip.CRC32 +@Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "MagicNumber", "ReturnCount") class FileSystemRepository(private val dao: FileSystemDao, private val context: Context) { companion object { @@ -25,7 +26,6 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: const val BATCH_SIZE = 50 } - @Suppress("NestedBlockDepth") suspend fun getFilePathsWithIds(syncedFolder: SyncedFolder, lastId: Int): List> { val syncedFolderId = syncedFolder.id.toString() Log_OC.d(TAG, "Fetching candidate files for syncedFolderId = $syncedFolderId") @@ -57,7 +57,6 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: return filtered } - @Suppress("TooGenericExceptionCaught") suspend fun markFileAsUploaded(localPath: String, syncedFolder: SyncedFolder) { val syncedFolderIdStr = syncedFolder.id.toString() @@ -71,16 +70,42 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: fun insert(uri: Uri, syncedFolder: SyncedFolder) { val projection = arrayOf(MediaStore.MediaColumns.DATA) - val cursor = context.contentResolver.query(uri, projection, null, null, null) + + var syncedPath = syncedFolder.localPath + if (syncedPath.isNullOrEmpty()) { + Log_OC.w(TAG, "Synced folder path is null or empty") + return + } + + if (!syncedPath.endsWith(File.separator)) { + syncedPath += File.separator + } + + val selection = "${MediaStore.MediaColumns.DATA} LIKE ?" + val selectionArgs = arrayOf("$syncedPath%") + + Log_OC.d(TAG, "Querying MediaStore for files in: $syncedPath") + + val cursor = context.contentResolver.query( + uri, + projection, + selection, + selectionArgs, + null + ) cursor?.use { val columnIndexData = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) + var count = 0 while (cursor.moveToNext()) { val filePath = cursor.getString(columnIndexData) - Log_OC.d(TAG, "attempt to insert new file entity uri: $uri") + Log_OC.d(TAG, "Found file in synced folder: $filePath") insertOrReplace(filePath, null, syncedFolder) + count++ } + + Log_OC.d(TAG, "Inserted $count files for syncedFolder: ${syncedFolder.id}") } ?: Log_OC.w(TAG, "Cursor is null for URI: $uri") } @@ -99,7 +124,11 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: val entity = dao.getFileByPathAndFolder(localPath, syncedFolder.id.toString()) if (entity != null && entity.fileSentForUpload == 1) { - Log_OC.w(TAG, "file already uploaded path: $localPath, syncedFolder: ${syncedFolder.localPath}, ${syncedFolder.id}") + Log_OC.w( + TAG, + "file already uploaded path: $localPath, " + + "syncedFolder: ${syncedFolder.localPath}, ${syncedFolder.id}" + ) return } @@ -124,19 +153,17 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: } } - private fun getFileChecksum(file: File): Long? { - return try { - file.inputStream().use { fis -> - val crc = CRC32() - val buffer = ByteArray(64 * 1024) - var bytesRead: Int - while (fis.read(buffer).also { bytesRead = it } > 0) { - crc.update(buffer, 0, bytesRead) - } - crc.value + private fun getFileChecksum(file: File): Long? = try { + file.inputStream().use { fis -> + val crc = CRC32() + val buffer = ByteArray(64 * 1024) + var bytesRead: Int + while (fis.read(buffer).also { bytesRead = it } > 0) { + crc.update(buffer, 0, bytesRead) } - } catch (_: Exception) { - null + crc.value } + } catch (_: Exception) { + null } } From ac8d6c314f8fcdc8ef0d4a4657764682e1ea8c3d Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 12:48:40 +0100 Subject: [PATCH 03/12] fix: file system provider Signed-off-by: alperozturk --- .../nextcloud/client/jobs/autoUpload/FileSystemRepository.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index 1a9438d14034..5984c5024ef5 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -104,8 +104,6 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: insertOrReplace(filePath, null, syncedFolder) count++ } - - Log_OC.d(TAG, "Inserted $count files for syncedFolder: ${syncedFolder.id}") } ?: Log_OC.w(TAG, "Cursor is null for URI: $uri") } From 5878ac1464d722e377b5f91fe3376cee1e1be79f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 13:01:10 +0100 Subject: [PATCH 04/12] fix: file system provider Signed-off-by: alperozturk --- .../com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt index febbfe383005..3f1d4e68474b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt @@ -9,7 +9,6 @@ package com.nextcloud.client.jobs.autoUpload import com.nextcloud.utils.extensions.shouldSkipFile import com.nextcloud.utils.extensions.toLocalPath -import com.owncloud.android.datamodel.FilesystemDataProvider import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.lib.common.utils.Log_OC import java.io.IOException From c21dd4394dea3517dcf0f5273d042cf6a872e155 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 13:33:53 +0100 Subject: [PATCH 05/12] obey folder configuration Signed-off-by: alperozturk --- .../jobs/autoUpload/AutoUploadHelper.kt | 9 +--- .../jobs/autoUpload/FileSystemRepository.kt | 53 +++++++++++++++---- .../android/utils/FilesSyncHelper.java | 21 +++++++- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt index 3f1d4e68474b..98dc1534aa90 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt @@ -7,7 +7,6 @@ package com.nextcloud.client.jobs.autoUpload -import com.nextcloud.utils.extensions.shouldSkipFile import com.nextcloud.utils.extensions.toLocalPath import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.lib.common.utils.Log_OC @@ -69,15 +68,9 @@ class AutoUploadHelper { val javaFile = file.toFile() val lastModified = attrs?.lastModifiedTime()?.toMillis() ?: javaFile.lastModified() val creationTime = attrs?.creationTime()?.toMillis() - - if (folder.shouldSkipFile(javaFile, lastModified, creationTime)) { - skipCount++ - return FileVisitResult.CONTINUE - } - val localPath = file.toLocalPath() - repository?.insertOrReplace(localPath, lastModified, folder) + repository?.insertOrReplace(localPath, lastModified, creationTime, folder) fileCount++ diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index 5984c5024ef5..39f9aa0c1fbb 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -12,6 +12,7 @@ import android.net.Uri import android.provider.MediaStore import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.database.entity.FilesystemEntity +import com.nextcloud.utils.extensions.shouldSkipFile import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.SyncedFolderUtils @@ -69,7 +70,11 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: } fun insert(uri: Uri, syncedFolder: SyncedFolder) { - val projection = arrayOf(MediaStore.MediaColumns.DATA) + val projection = arrayOf( + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.MediaColumns.DATE_ADDED + ) var syncedPath = syncedFolder.localPath if (syncedPath.isNullOrEmpty()) { @@ -95,19 +100,41 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: ) cursor?.use { - val columnIndexData = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) - var count = 0 + val idxData = cursor.getColumnIndex(MediaStore.MediaColumns.DATA) + val idxModified = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED) + val idxAdded = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED) + + if (idxData == -1) { + Log_OC.e(TAG, "MediaStore column DATA missing — cannot process URI: $uri") + return + } while (cursor.moveToNext()) { - val filePath = cursor.getString(columnIndexData) - Log_OC.d(TAG, "Found file in synced folder: $filePath") - insertOrReplace(filePath, null, syncedFolder) - count++ + val filePath = cursor.getString(idxData) + + val lastModifiedMs = + if (idxModified != -1) cursor.getLong(idxModified) * 1000 + else File(filePath).lastModified().also { + Log_OC.w(TAG, "DATE_MODIFIED missing, fallback to File.lastModified() for $filePath") + } + + val creationTimeMs = + if (idxAdded != -1) cursor.getLong(idxAdded) * 1000 + else null.also { + Log_OC.w(TAG, "DATE_ADDED missing, creationTime=null for $filePath") + } + + Log_OC.d( + TAG, + "Found file: $filePath (created=$creationTimeMs, modified=$lastModifiedMs)" + ) + + insertOrReplace(filePath, lastModifiedMs, creationTimeMs, syncedFolder) } - } ?: Log_OC.w(TAG, "Cursor is null for URI: $uri") + } } - fun insertOrReplace(localPath: String?, lastModified: Long?, syncedFolder: SyncedFolder) { + fun insertOrReplace(localPath: String?, lastModified: Long?, creationTime: Long?, syncedFolder: SyncedFolder) { try { if (localPath == null) { Log_OC.w(TAG, "localPath path not exists: $localPath") @@ -120,6 +147,12 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: return } + val fileModified = (lastModified ?: file.lastModified()) + val shouldSkipFileBasedOnFolderSettings = syncedFolder.shouldSkipFile(file, fileModified, creationTime) + if (shouldSkipFileBasedOnFolderSettings) { + return + } + val entity = dao.getFileByPathAndFolder(localPath, syncedFolder.id.toString()) if (entity != null && entity.fileSentForUpload == 1) { Log_OC.w( @@ -140,7 +173,7 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: fileSentForUpload = 0, syncedFolderId = syncedFolder.id.toString(), crc32 = crc?.toString(), - fileModified = lastModified ?: file.lastModified() + fileModified = fileModified ) Log_OC.d(TAG, "inserting new file system entity: $newEntity") diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java index 6d29d58f5b2a..8d90706a2604 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java @@ -10,6 +10,7 @@ package com.owncloud.android.utils; import android.content.Context; +import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; @@ -114,7 +115,25 @@ public static boolean insertChangedEntries(SyncedFolder syncedFolder, String[] c return false; } - repository.insertOrReplace(filePath, file.lastModified(), syncedFolder); + Long creationTimeMs = null; + String[] projection = { MediaStore.MediaColumns.DATE_ADDED }; + try (Cursor cursor = context.getContentResolver() + .query(contentUri, projection, null, null, null)) { + + if (cursor != null && cursor.moveToFirst()) { + int idxAdded = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED); + if (idxAdded != -1) { + long dateAddedSec = cursor.getLong(idxAdded); + creationTimeMs = dateAddedSec * 1000; + } else { + Log_OC.w(TAG, "DATE_ADDED missing for: " + filePath); + } + } + } catch (Exception e) { + Log_OC.w(TAG, "Could not query creation time for: " + filePath); + } + + repository.insertOrReplace(filePath, file.lastModified(), creationTimeMs, syncedFolder); } Log_OC.d(TAG, "changed content uris successfully stored"); From e34791a9c2f3c2532ca125f36f31103a378a86fc Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 13:34:10 +0100 Subject: [PATCH 06/12] obey folder configuration Signed-off-by: alperozturk --- .../jobs/autoUpload/FileSystemRepository.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index 39f9aa0c1fbb..9d5f73b7a382 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -113,15 +113,21 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: val filePath = cursor.getString(idxData) val lastModifiedMs = - if (idxModified != -1) cursor.getLong(idxModified) * 1000 - else File(filePath).lastModified().also { - Log_OC.w(TAG, "DATE_MODIFIED missing, fallback to File.lastModified() for $filePath") + if (idxModified != -1) { + cursor.getLong(idxModified) * 1000 + } else { + File(filePath).lastModified().also { + Log_OC.w(TAG, "DATE_MODIFIED missing, fallback to File.lastModified() for $filePath") + } } val creationTimeMs = - if (idxAdded != -1) cursor.getLong(idxAdded) * 1000 - else null.also { - Log_OC.w(TAG, "DATE_ADDED missing, creationTime=null for $filePath") + if (idxAdded != -1) { + cursor.getLong(idxAdded) * 1000 + } else { + null.also { + Log_OC.w(TAG, "DATE_ADDED missing, creationTime=null for $filePath") + } } Log_OC.d( From e083257228adf464c05accd303c516ca4669fb20 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 13:39:45 +0100 Subject: [PATCH 07/12] obey folder configuration Signed-off-by: alperozturk --- .../jobs/autoUpload/FileSystemRepository.kt | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index 9d5f73b7a382..2106f3041511 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -112,23 +112,17 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: while (cursor.moveToNext()) { val filePath = cursor.getString(idxData) - val lastModifiedMs = - if (idxModified != -1) { - cursor.getLong(idxModified) * 1000 - } else { - File(filePath).lastModified().also { - Log_OC.w(TAG, "DATE_MODIFIED missing, fallback to File.lastModified() for $filePath") - } - } + val lastModifiedMs = if (idxModified != -1) { + cursor.getLong(idxModified) * 1000 + } else { + null + } - val creationTimeMs = - if (idxAdded != -1) { - cursor.getLong(idxAdded) * 1000 - } else { - null.also { - Log_OC.w(TAG, "DATE_ADDED missing, creationTime=null for $filePath") - } - } + val creationTimeMs = if (idxAdded != -1) { + cursor.getLong(idxAdded) * 1000 + } else { + null + } Log_OC.d( TAG, From ef9535e12cf6cf09090ee91314d3cbcbddeb61a1 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 14:20:15 +0100 Subject: [PATCH 08/12] deduplicate logic Signed-off-by: alperozturk --- .../jobs/autoUpload/FileSystemRepository.kt | 24 +++++--- .../utils/extensions/UriExtensions.kt | 43 --------------- .../android/utils/FilesSyncHelper.java | 55 ++----------------- 3 files changed, 22 insertions(+), 100 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/utils/extensions/UriExtensions.kt diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index 2106f3041511..f3a4dc9c9668 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -13,6 +13,7 @@ import android.provider.MediaStore import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.database.entity.FilesystemEntity import com.nextcloud.utils.extensions.shouldSkipFile +import com.nextcloud.utils.extensions.toFile import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.SyncedFolderUtils @@ -69,7 +70,8 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: } } - fun insert(uri: Uri, syncedFolder: SyncedFolder) { + @JvmOverloads + fun insertFromUri(uri: Uri, syncedFolder: SyncedFolder, checkFileType: Boolean = false) { val projection = arrayOf( MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DATE_MODIFIED, @@ -129,21 +131,27 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: "Found file: $filePath (created=$creationTimeMs, modified=$lastModifiedMs)" ) - insertOrReplace(filePath, lastModifiedMs, creationTimeMs, syncedFolder) + insertOrReplace(filePath, lastModifiedMs, creationTimeMs, syncedFolder, checkFileType) } } } - fun insertOrReplace(localPath: String?, lastModified: Long?, creationTime: Long?, syncedFolder: SyncedFolder) { + fun insertOrReplace( + localPath: String?, + lastModified: Long?, + creationTime: Long?, + syncedFolder: SyncedFolder, + checkFileType: Boolean = false + ) { try { - if (localPath == null) { - Log_OC.w(TAG, "localPath path not exists: $localPath") + val file = localPath?.toFile() + if (file == null) { + Log_OC.w(TAG, "file null, cannot insert or replace: $localPath") return } - val file = File(localPath) - if (!file.exists()) { - Log_OC.w(TAG, "local file does not exist, cannot insert or replace: $localPath") + if (checkFileType && !syncedFolder.containsTypedFile(file, localPath)) { + Log_OC.w(TAG, "synced folder not contains typed file: $localPath") return } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UriExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UriExtensions.kt deleted file mode 100644 index 174cf97102e1..000000000000 --- a/app/src/main/java/com/nextcloud/utils/extensions/UriExtensions.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.utils.extensions - -import android.content.Context -import android.net.Uri -import android.provider.MediaStore -import com.owncloud.android.lib.common.utils.Log_OC - -/** - * Returns absolute filesystem path to the media item on disk. I/O errors that could occur. From Android 11 onwards, - * this column is read-only for apps that target R and higher. - * - * [More Info](https://developer.android.com/reference/android/provider/MediaStore.MediaColumns#DATA) - */ -@Suppress("ReturnCount", "TooGenericExceptionCaught") -fun Uri.toFilePath(context: Context): String? { - try { - val projection = arrayOf(MediaStore.MediaColumns.DATA) - - val resolver = context.contentResolver - - resolver.query(this, projection, null, null, null)?.use { cursor -> - if (!cursor.moveToFirst()) { - return null - } - - val dataIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DATA) - val data = if (dataIdx != -1) cursor.getString(dataIdx) else null - return data - } - - return null - } catch (e: Exception) { - Log_OC.e("UriExtensions", "exception, toFilePath: $e") - return null - } -} diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java index 8d90706a2604..0f7dffe27c27 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java @@ -10,7 +10,6 @@ package com.owncloud.android.utils; import android.content.Context; -import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; @@ -22,16 +21,12 @@ import com.nextcloud.client.jobs.autoUpload.FileSystemRepository; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.network.ConnectivityService; -import com.nextcloud.utils.extensions.UriExtensionsKt; -import com.owncloud.android.MainApp; import com.owncloud.android.datamodel.MediaFolderType; import com.owncloud.android.datamodel.SyncedFolder; import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.UploadsStorageManager; import com.owncloud.android.lib.common.utils.Log_OC; -import java.io.File; - /** * Various utilities that make auto upload tick */ @@ -51,11 +46,11 @@ public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, MediaFolderType mediaType = syncedFolder.getType(); if (mediaType == MediaFolderType.IMAGE) { - repository.insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, syncedFolder); - repository.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, syncedFolder); + repository.insertFromUri(MediaStore.Images.Media.INTERNAL_CONTENT_URI, syncedFolder); + repository.insertFromUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, syncedFolder); } else if (mediaType == MediaFolderType.VIDEO) { - repository.insert(MediaStore.Video.Media.INTERNAL_CONTENT_URI, syncedFolder); - repository.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, syncedFolder); + repository.insertFromUri(MediaStore.Video.Media.INTERNAL_CONTENT_URI, syncedFolder); + repository.insertFromUri(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, syncedFolder); } else { helper.insertCustomFolderIntoDB(syncedFolder, repository); } @@ -83,57 +78,19 @@ public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, * @return {@code true} if all changed content URIs were successfully stored; {@code false} otherwise. */ public static boolean insertChangedEntries(SyncedFolder syncedFolder, String[] contentUris, FileSystemRepository repository) { - final Context context = MainApp.getAppContext(); for (String contentUriString : contentUris) { if (contentUriString == null) { Log_OC.w(TAG, "null content uri string"); return false; } - Uri contentUri; try { - contentUri = Uri.parse(contentUriString); + Uri contentUri = Uri.parse(contentUriString); + repository.insertFromUri(contentUri, syncedFolder, true); } catch (Exception e) { Log_OC.e(TAG, "Invalid URI: " + contentUriString, e); return false; } - - String filePath = UriExtensionsKt.toFilePath(contentUri, context); - if (filePath == null) { - Log_OC.w(TAG, "File path is null"); - return false; - } - - File file = new File(filePath); - if (!file.exists()) { - Log_OC.w(TAG, "syncedFolder contains not existing changed file: " + filePath); - return false; - } - - if (!syncedFolder.containsTypedFile(file, filePath)) { - Log_OC.w(TAG, "syncedFolder not contains typed file, changedFile: " + filePath); - return false; - } - - Long creationTimeMs = null; - String[] projection = { MediaStore.MediaColumns.DATE_ADDED }; - try (Cursor cursor = context.getContentResolver() - .query(contentUri, projection, null, null, null)) { - - if (cursor != null && cursor.moveToFirst()) { - int idxAdded = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED); - if (idxAdded != -1) { - long dateAddedSec = cursor.getLong(idxAdded); - creationTimeMs = dateAddedSec * 1000; - } else { - Log_OC.w(TAG, "DATE_ADDED missing for: " + filePath); - } - } - } catch (Exception e) { - Log_OC.w(TAG, "Could not query creation time for: " + filePath); - } - - repository.insertOrReplace(filePath, file.lastModified(), creationTimeMs, syncedFolder); } Log_OC.d(TAG, "changed content uris successfully stored"); From 58b39bb45b73ee94b2c406eba1de3efec9243a26 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 14:32:10 +0100 Subject: [PATCH 09/12] deduplicate logic Signed-off-by: alperozturk --- .../jobs/autoUpload/AutoUploadHelper.kt | 66 ++++++++++++++++++- .../jobs/autoUpload/AutoUploadWorker.kt | 7 +- .../android/utils/FilesSyncHelper.java | 65 ------------------ 3 files changed, 67 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt index 98dc1534aa90..15b23e20fbe7 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt @@ -7,7 +7,10 @@ package com.nextcloud.client.jobs.autoUpload +import android.provider.MediaStore +import androidx.core.net.toUri import com.nextcloud.utils.extensions.toLocalPath +import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.lib.common.utils.Log_OC import java.io.IOException @@ -27,7 +30,66 @@ class AutoUploadHelper { private const val MAX_DEPTH = 100 } - fun insertCustomFolderIntoDB(folder: SyncedFolder, repository: FileSystemRepository?): Int { + fun insertEntries(folder: SyncedFolder, repository: FileSystemRepository) { + val enabledTimestampMs = folder.enabledTimestampMs + if (!folder.isEnabled || (!folder.isExisting && enabledTimestampMs < 0)) { + Log_OC.w( + TAG, + "Skipping insertDBEntries: enabled=${folder.isEnabled}, " + + "exists=${folder.isExisting}, enabledTs=$enabledTimestampMs" + ) + return + } + + when (folder.type) { + MediaFolderType.IMAGE -> { + repository.insertFromUri(MediaStore.Images.Media.INTERNAL_CONTENT_URI, folder) + repository.insertFromUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, folder) + } + + MediaFolderType.VIDEO -> { + repository.insertFromUri(MediaStore.Video.Media.INTERNAL_CONTENT_URI, folder) + repository.insertFromUri(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, folder) + } + + else -> { + insertCustomFolderIntoDB(folder, repository) + } + } + } + + /** + * Attempts to get the file path from a content URI string (e.g., content://media/external/images/media/2281) + * and checks its type. If the conditions are met, the file is stored for auto-upload. + *

+ * If any attempt fails, the method returns {@code false}. + * + * @param syncedFolder The folder marked for auto-upload. + * @param contentUris An array of content URI strings collected from + * {@link ContentObserverWork##checkAndTriggerAutoUpload()}. + * @return {@code true} if all changed content URIs were successfully stored; {@code false} otherwise. + */ + fun insertChangedEntries( + syncedFolder: SyncedFolder, + contentUris: Array?, + repository: FileSystemRepository + ): Boolean { + contentUris?.forEach { uriString -> + try { + val uri = uriString.toUri() + repository.insertFromUri(uri, syncedFolder, true) + } catch (e: Exception) { + Log_OC.e(TAG, "Invalid URI: $uriString", e) + return false + } + } + + Log_OC.d(TAG, "Changed content URIs successfully stored") + + return true + } + + fun insertCustomFolderIntoDB(folder: SyncedFolder, repository: FileSystemRepository): Int { val path = Paths.get(folder.localPath) if (!Files.exists(path)) { @@ -70,7 +132,7 @@ class AutoUploadHelper { val creationTime = attrs?.creationTime()?.toMillis() val localPath = file.toLocalPath() - repository?.insertOrReplace(localPath, lastModified, creationTime, folder) + repository.insertOrReplace(localPath, lastModified, creationTime, folder) fileCount++ diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index 8ae9d481af2d..e1fc93df33bc 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -42,7 +42,6 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.activity.SettingsActivity import com.owncloud.android.utils.FileStorageUtils -import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.MimeType import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers @@ -230,16 +229,16 @@ class AutoUploadWorker( private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array?) = try { withContext(Dispatchers.IO) { if (contentUris.isNullOrEmpty()) { - FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper, repository) + helper.insertEntries(syncedFolder, repository) } else { - val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris, repository) + val isContentUrisStored = helper.insertChangedEntries(syncedFolder, contentUris, repository) if (!isContentUrisStored) { Log_OC.w( TAG, "changed content uris not stored, fallback to insert all db entries to not lose files" ) - FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper, repository) + helper.insertEntries(syncedFolder, repository) } } syncedFolder.lastScanTimestampMs = System.currentTimeMillis() diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java index 0f7dffe27c27..60ac53201200 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java @@ -10,18 +10,12 @@ package com.owncloud.android.utils; import android.content.Context; -import android.net.Uri; -import android.provider.MediaStore; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.device.PowerManagementService; import com.nextcloud.client.jobs.BackgroundJobManager; -import com.nextcloud.client.jobs.ContentObserverWork; -import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper; -import com.nextcloud.client.jobs.autoUpload.FileSystemRepository; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.network.ConnectivityService; -import com.owncloud.android.datamodel.MediaFolderType; import com.owncloud.android.datamodel.SyncedFolder; import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.UploadsStorageManager; @@ -39,65 +33,6 @@ private FilesSyncHelper() { // utility class -> private constructor } - public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, AutoUploadHelper helper, FileSystemRepository repository) { - final long enabledTimestampMs = syncedFolder.getEnabledTimestampMs(); - - if (syncedFolder.isEnabled() && (syncedFolder.isExisting() || enabledTimestampMs >= 0)) { - MediaFolderType mediaType = syncedFolder.getType(); - - if (mediaType == MediaFolderType.IMAGE) { - repository.insertFromUri(MediaStore.Images.Media.INTERNAL_CONTENT_URI, syncedFolder); - repository.insertFromUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, syncedFolder); - } else if (mediaType == MediaFolderType.VIDEO) { - repository.insertFromUri(MediaStore.Video.Media.INTERNAL_CONTENT_URI, syncedFolder); - repository.insertFromUri(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, syncedFolder); - } else { - helper.insertCustomFolderIntoDB(syncedFolder, repository); - } - } else { - if (!syncedFolder.isEnabled()) { - Log_OC.w(TAG, "insertAllDBEntriesForSyncedFolder, syncedFolder not enabled"); - } - - if (!syncedFolder.isExisting()) { - Log_OC.w(TAG, "insertAllDBEntriesForSyncedFolder, syncedFolder is not exists"); - } - - Log_OC.w(TAG, "insertAllDBEntriesForSyncedFolder, enabledTimestampMs: " + enabledTimestampMs); - } - } - - /** - * Attempts to get the file path from a content URI string (e.g., content://media/external/images/media/2281) - * and checks its type. If the conditions are met, the file is stored for auto-upload. - *

- * If any attempt fails, the method returns {@code false}. - * - * @param syncedFolder The folder marked for auto-upload. - * @param contentUris An array of content URI strings collected from {@link ContentObserverWork##checkAndTriggerAutoUpload()}. - * @return {@code true} if all changed content URIs were successfully stored; {@code false} otherwise. - */ - public static boolean insertChangedEntries(SyncedFolder syncedFolder, String[] contentUris, FileSystemRepository repository) { - for (String contentUriString : contentUris) { - if (contentUriString == null) { - Log_OC.w(TAG, "null content uri string"); - return false; - } - - try { - Uri contentUri = Uri.parse(contentUriString); - repository.insertFromUri(contentUri, syncedFolder, true); - } catch (Exception e) { - Log_OC.e(TAG, "Invalid URI: " + contentUriString, e); - return false; - } - } - - Log_OC.d(TAG, "changed content uris successfully stored"); - - return true; - } - public static void restartUploadsIfNeeded(final UploadsStorageManager uploadsStorageManager, final UserAccountManager accountManager, final ConnectivityService connectivityService, From 3d16819f7ec74ade1ae357a64651a5ee978fae4f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 14:38:28 +0100 Subject: [PATCH 10/12] deduplicate logic Signed-off-by: alperozturk --- .../nextcloud/client/jobs/autoUpload/FileSystemRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index f3a4dc9c9668..8cb6a74dc1ec 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -91,7 +91,7 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: val selection = "${MediaStore.MediaColumns.DATA} LIKE ?" val selectionArgs = arrayOf("$syncedPath%") - Log_OC.d(TAG, "Querying MediaStore for files in: $syncedPath") + Log_OC.d(TAG, "Querying MediaStore for files in: $syncedPath, uri: $uri") val cursor = context.contentResolver.query( uri, From 7a675e29e876b8d28e8f0a3f96cb56b1a3e661ba Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 15:20:04 +0100 Subject: [PATCH 11/12] add repo to tests Signed-off-by: alperozturk --- .../extensions/SyncedFolderExtensions.kt | 2 ++ .../android/utils/AutoUploadHelperTest.kt | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt index 5251aa341812..c7bdadfe6f54 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt @@ -23,6 +23,8 @@ private const val TAG = "SyncedFolderExtensions" */ @Suppress("ReturnCount") fun SyncedFolder.shouldSkipFile(file: File, lastModified: Long, creationTime: Long?): Boolean { + Log_OC.d(TAG, "Checking file: ${file.name}, lastModified=$lastModified, lastScan=$lastScanTimestampMs") + if (isExcludeHidden && file.isHidden) { Log_OC.d(TAG, "Skipping hidden: ${file.absolutePath}") return true diff --git a/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt b/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt index 1eb86275abb1..d76502ab8f82 100644 --- a/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt +++ b/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt @@ -7,10 +7,15 @@ package com.owncloud.android.utils +import android.content.Context +import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper +import com.nextcloud.client.jobs.autoUpload.FileSystemRepository import com.nextcloud.client.preferences.SubFolderRule import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder +import io.mockk.clearAllMocks +import io.mockk.mockk import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -27,16 +32,24 @@ class AutoUploadHelperTest { private val helper = AutoUploadHelper() private val accountName = "testAccount" + private val mockDao: FileSystemDao = mockk(relaxed = true) + private val mockContext: Context = mockk(relaxed = true) + + private lateinit var repo: FileSystemRepository + @Before fun setup() { tempDir = Files.createTempDirectory("auto_upload_test_").toFile() tempDir.mkdirs() assertTrue("Failed to create temp directory", tempDir.exists()) + + repo = FileSystemRepository(mockDao, mockContext) } @After fun cleanup() { tempDir.deleteRecursively() + clearAllMocks() } private fun createTestFolder( @@ -81,7 +94,7 @@ class AutoUploadHelperTest { type = MediaFolderType.CUSTOM ) - val processedCount = helper.insertCustomFolderIntoDB(folder, null) + val processedCount = helper.insertCustomFolderIntoDB(folder, repo) assertEquals("Should process 2 files", 2, processedCount) } @@ -98,7 +111,7 @@ class AutoUploadHelperTest { type = MediaFolderType.CUSTOM ) - val processedCount = helper.insertCustomFolderIntoDB(folder, null) + val processedCount = helper.insertCustomFolderIntoDB(folder, repo) assertTrue("Should process at least 1 file", processedCount >= 1) } @@ -120,7 +133,7 @@ class AutoUploadHelperTest { type = MediaFolderType.CUSTOM ) - val processedCount = helper.insertCustomFolderIntoDB(folder, null) + val processedCount = helper.insertCustomFolderIntoDB(folder, repo) // Should only process the new file (modified after last scan) assertEquals("Should process only 1 new file", 1, processedCount) @@ -145,7 +158,7 @@ class AutoUploadHelperTest { lastScanTimestampMs = currentTime } - val processedCount = helper.insertCustomFolderIntoDB(folder, null) + val processedCount = helper.insertCustomFolderIntoDB(folder, repo) // Should only process files newer than enabledTimestamp assertEquals("Should process only files after enabled timestamp", 1, processedCount) @@ -154,7 +167,7 @@ class AutoUploadHelperTest { @Test fun testInsertCustomFolderEmpty() { val folder = createTestFolder(type = MediaFolderType.CUSTOM) - val processedCount = helper.insertCustomFolderIntoDB(folder, null) + val processedCount = helper.insertCustomFolderIntoDB(folder, repo) assertEquals("Empty folder should process 0 files", 0, processedCount) } @@ -167,7 +180,7 @@ class AutoUploadHelperTest { type = MediaFolderType.CUSTOM ) - val processedCount = helper.insertCustomFolderIntoDB(folder, null) + val processedCount = helper.insertCustomFolderIntoDB(folder, repo) assertEquals("Non-existent folder should return 0", 0, processedCount) } @@ -181,7 +194,7 @@ class AutoUploadHelperTest { File(subDir, "nested.txt").writeText("Nested file") val folder = createTestFolder(type = MediaFolderType.CUSTOM) - val processedCount = helper.insertCustomFolderIntoDB(folder, null) + val processedCount = helper.insertCustomFolderIntoDB(folder, repo) assertEquals("Should process files in root and subdirectories", 2, processedCount) } @@ -209,7 +222,7 @@ class AutoUploadHelperTest { lastScanTimestampMs = currentTime } - val processedCount = helper.insertCustomFolderIntoDB(folder, null) + val processedCount = helper.insertCustomFolderIntoDB(folder, repo) // Should skip hidden directory and its contents assertEquals("Should only process regular file", 1, processedCount) @@ -262,7 +275,7 @@ class AutoUploadHelperTest { * ${tempDir.absolutePath}/FOLDER_A/FOLDER_B/FOLDER_D/FOLDER_E/FILE_A.txt -> 1 * Total = 5 files */ - val processedCount = helper.insertCustomFolderIntoDB(syncedFolder, null) + val processedCount = helper.insertCustomFolderIntoDB(syncedFolder, repo) assertEquals("Should process all files in complex nested structure", 5, processedCount) } @@ -303,7 +316,7 @@ class AutoUploadHelperTest { setEnabled(true, currentTime) } - val processedCountSkipOld = helper.insertCustomFolderIntoDB(folderSkipOld, null) + val processedCountSkipOld = helper.insertCustomFolderIntoDB(folderSkipOld, repo) assertEquals( "When 'also upload existing' is disabled, only new files created after enabling should be processed", 1, @@ -318,7 +331,7 @@ class AutoUploadHelperTest { setEnabled(true, currentTime) } - val processedCountAll = helper.insertCustomFolderIntoDB(folderUploadAll, null) + val processedCountAll = helper.insertCustomFolderIntoDB(folderUploadAll, repo) assertEquals( "When 'also upload existing' is enabled, should upload all files", 2, From 1b28c0782d0ce46efbe069fd05a60fbe0683da9f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 12 Dec 2025 15:58:30 +0100 Subject: [PATCH 12/12] fix tests Signed-off-by: alperozturk --- .../android/utils/AutoUploadHelperTest.kt | 83 ++++++++----------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt b/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt index d76502ab8f82..993ee41a2915 100644 --- a/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt +++ b/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt @@ -12,6 +12,7 @@ import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper import com.nextcloud.client.jobs.autoUpload.FileSystemRepository import com.nextcloud.client.preferences.SubFolderRule +import com.nextcloud.utils.extensions.shouldSkipFile import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder import io.mockk.clearAllMocks @@ -23,7 +24,6 @@ import org.junit.Before import org.junit.Test import java.io.File import java.nio.file.Files -import java.nio.file.attribute.FileTime @Suppress("MagicNumber") class AutoUploadHelperTest { @@ -122,21 +122,21 @@ class AutoUploadHelperTest { // Create an old file val oldFile = File(tempDir, "old.txt").apply { writeText("Old") } - oldFile.setLastModified(currentTime - 10000) // 10 seconds ago + val oldFileLastModified = currentTime - 10000 // 10 seconds ago // Create a new file val newFile = File(tempDir, "new.txt").apply { writeText("New") } - newFile.setLastModified(currentTime) val folder = createTestFolder( lastScan = currentTime - 5000, // Last scan was 5 seconds ago type = MediaFolderType.CUSTOM ) - val processedCount = helper.insertCustomFolderIntoDB(folder, repo) + val shouldSkipOldFile = folder.shouldSkipFile(oldFile, oldFileLastModified, null) + assertTrue(shouldSkipOldFile) - // Should only process the new file (modified after last scan) - assertEquals("Should process only 1 new file", 1, processedCount) + val shouldSkipNewFile = folder.shouldSkipFile(newFile, currentTime, null) + assertTrue(!shouldSkipNewFile) } @Test @@ -145,10 +145,9 @@ class AutoUploadHelperTest { // old file should not be scanned val oldFile = File(tempDir, "old.txt").apply { writeText("Old") } - oldFile.setLastModified(currentTime - 10000) + val oldFileLastModified = currentTime - 10000 // 10 seconds ago val newFile = File(tempDir, "new.txt").apply { writeText("New") } - newFile.setLastModified(currentTime) // Enabled 5 seconds ago val folder = createTestFolder( @@ -158,10 +157,11 @@ class AutoUploadHelperTest { lastScanTimestampMs = currentTime } - val processedCount = helper.insertCustomFolderIntoDB(folder, repo) + val shouldSkipOldFile = folder.shouldSkipFile(oldFile, oldFileLastModified, null) + assertTrue(shouldSkipOldFile) - // Should only process files newer than enabledTimestamp - assertEquals("Should process only files after enabled timestamp", 1, processedCount) + val shouldSkipNewFile = folder.shouldSkipFile(newFile, currentTime, null) + assertTrue(!shouldSkipNewFile) } @Test @@ -266,15 +266,15 @@ class AutoUploadHelperTest { type = MediaFolderType.CUSTOM ) - /* - * Expected file count with full paths: - * ${tempDir.absolutePath}/FOLDER_A/FILE_A.txt -> 1 - * ${tempDir.absolutePath}/FOLDER_A/FOLDER_B/FILE_B.txt -> 1 - * ${tempDir.absolutePath}/FOLDER_A/FOLDER_C/FILE_A.txt -> 1 - * ${tempDir.absolutePath}/FOLDER_A/FOLDER_C/FILE_B.txt -> 1 - * ${tempDir.absolutePath}/FOLDER_A/FOLDER_B/FOLDER_D/FOLDER_E/FILE_A.txt -> 1 - * Total = 5 files - */ + /* + * Expected file count with full paths: + * ${tempDir.absolutePath}/FOLDER_A/FILE_A.txt -> 1 + * ${tempDir.absolutePath}/FOLDER_A/FOLDER_B/FILE_B.txt -> 1 + * ${tempDir.absolutePath}/FOLDER_A/FOLDER_C/FILE_A.txt -> 1 + * ${tempDir.absolutePath}/FOLDER_A/FOLDER_C/FILE_B.txt -> 1 + * ${tempDir.absolutePath}/FOLDER_A/FOLDER_B/FOLDER_D/FOLDER_E/FILE_A.txt -> 1 + * Total = 5 files + */ val processedCount = helper.insertCustomFolderIntoDB(syncedFolder, repo) assertEquals("Should process all files in complex nested structure", 5, processedCount) } @@ -287,26 +287,15 @@ class AutoUploadHelperTest { val oldFile = File(tempDir, "old_file.txt").apply { writeText("Old file") } - - val oldFilePath = oldFile.toPath() - Files.setAttribute( - oldFilePath, - "creationTime", - FileTime.fromMillis(currentTime - 60_000) // 1 minute before enabling - ) - - Thread.sleep(1000) + val oldFileCreationTime = currentTime - 10_000 + val oldFileLastModified = currentTime - 5_000 // New file (created after enabling auto-upload) val newFile = File(tempDir, "new_file.txt").apply { writeText("New file") } - val newFilePath = newFile.toPath() - Files.setAttribute( - newFilePath, - "creationTime", - FileTime.fromMillis(currentTime + 60_000) // 1 minute after enabling - ) + val newFileCreationTime = currentTime + 10_000 + val newFileLastModified = currentTime + 5_000 val folderSkipOld = createTestFolder( localPath = tempDir.absolutePath, @@ -316,12 +305,11 @@ class AutoUploadHelperTest { setEnabled(true, currentTime) } - val processedCountSkipOld = helper.insertCustomFolderIntoDB(folderSkipOld, repo) - assertEquals( - "When 'also upload existing' is disabled, only new files created after enabling should be processed", - 1, - processedCountSkipOld - ) + val shouldSkipOldFile = folderSkipOld.shouldSkipFile(oldFile, oldFileLastModified, oldFileCreationTime) + assertTrue(shouldSkipOldFile) + + val shouldSkipNewFile = folderSkipOld.shouldSkipFile(newFile, newFileLastModified, newFileCreationTime) + assertTrue(!shouldSkipNewFile) val folderUploadAll = createTestFolder( localPath = tempDir.absolutePath, @@ -331,11 +319,12 @@ class AutoUploadHelperTest { setEnabled(true, currentTime) } - val processedCountAll = helper.insertCustomFolderIntoDB(folderUploadAll, repo) - assertEquals( - "When 'also upload existing' is enabled, should upload all files", - 2, - processedCountAll - ) + val shouldSkipOldFileIfAlsoUploadExistingFile = + folderUploadAll.shouldSkipFile(oldFile, oldFileLastModified, oldFileCreationTime) + assertTrue(!shouldSkipOldFileIfAlsoUploadExistingFile) + + val shouldSkipNewFileIfAlsoUploadExistingFile = + folderUploadAll.shouldSkipFile(newFile, newFileLastModified, newFileCreationTime) + assertTrue(!shouldSkipNewFileIfAlsoUploadExistingFile) } }