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..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,9 +7,10 @@ package com.nextcloud.client.jobs.autoUpload -import com.nextcloud.utils.extensions.shouldSkipFile +import android.provider.MediaStore +import androidx.core.net.toUri import com.nextcloud.utils.extensions.toLocalPath -import com.owncloud.android.datamodel.FilesystemDataProvider +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 @@ -29,7 +30,66 @@ class AutoUploadHelper { private const val MAX_DEPTH = 100 } - fun insertCustomFolderIntoDB(folder: SyncedFolder, filesystemDataProvider: FilesystemDataProvider?): 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
- * 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) {
- Log_OC.d(TAG, "insertChangedEntries, syncedFolderID: " + syncedFolder.getId());
- 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");
- return false;
- }
-
- Uri contentUri;
- try {
- contentUri = Uri.parse(contentUriString);
- } 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;
- }
-
- filesystemDataProvider.storeOrUpdateFileValue(filePath, file.lastModified(), file.isDirectory(), syncedFolder);
- }
-
- Log_OC.d(TAG, "changed content uris successfully stored");
-
- 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,
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..993ee41a2915 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,16 @@
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.nextcloud.utils.extensions.shouldSkipFile
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
@@ -18,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 {
@@ -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)
}
@@ -109,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, null)
+ 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
@@ -132,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(
@@ -145,16 +157,17 @@ class AutoUploadHelperTest {
lastScanTimestampMs = currentTime
}
- val processedCount = helper.insertCustomFolderIntoDB(folder, null)
+ 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
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)
@@ -253,16 +266,16 @@ 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
- */
- val processedCount = helper.insertCustomFolderIntoDB(syncedFolder, null)
+ /*
+ * 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)
}
@@ -274,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,
@@ -303,12 +305,11 @@ class AutoUploadHelperTest {
setEnabled(true, currentTime)
}
- val processedCountSkipOld = helper.insertCustomFolderIntoDB(folderSkipOld, null)
- 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,
@@ -318,11 +319,12 @@ class AutoUploadHelperTest {
setEnabled(true, currentTime)
}
- val processedCountAll = helper.insertCustomFolderIntoDB(folderUploadAll, null)
- 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)
}
}