Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,10 @@ interface BackgroundJobManager {

fun schedulePeriodicFilesSyncJob(syncedFolder: SyncedFolder)

/**
* Immediately start File Sync job for given syncFolderID.
*/
fun startImmediateFilesSyncJob(
fun startAutoUploadImmediately(
syncedFolder: SyncedFolder,
overridePowerSaving: Boolean = false,
changedFiles: Array<String?> = arrayOf()
contentUris: Array<String?> = arrayOf()
)

fun cancelTwoWaySyncJob()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,16 +507,16 @@ internal class BackgroundJobManagerImpl(
)
}

override fun startImmediateFilesSyncJob(
override fun startAutoUploadImmediately(
syncedFolder: SyncedFolder,
overridePowerSaving: Boolean,
changedFiles: Array<String?>
contentUris: Array<String?>
) {
val syncedFolderID = syncedFolder.id

val arguments = Data.Builder()
.putBoolean(AutoUploadWorker.OVERRIDE_POWER_SAVING, overridePowerSaving)
.putStringArray(AutoUploadWorker.CHANGED_FILES, changedFiles)
.putStringArray(AutoUploadWorker.CONTENT_URIS, contentUris)
.putLong(AutoUploadWorker.SYNCED_FOLDER_ID, syncedFolderID)
.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ContentObserverWork(

if (params.triggeredContentUris.isNotEmpty()) {
Log_OC.d(TAG, "File-sync Content Observer detected files change")
checkAndStartFileSyncJob()
checkAndTriggerAutoUpload()
backgroundJobManager.startMediaFoldersDetectionJob()
} else {
Log_OC.d(TAG, "triggeredContentUris empty")
Expand All @@ -50,20 +50,21 @@ class ContentObserverWork(
backgroundJobManager.scheduleContentObserverJob()
}

private fun checkAndStartFileSyncJob() {
private fun checkAndTriggerAutoUpload() {
if (!powerManagementService.isPowerSavingEnabled && syncedFolderProvider.countEnabledSyncedFolders() > 0) {
val changedFiles = mutableListOf<String>()
val contentUris = mutableListOf<String>()
for (uri in params.triggeredContentUris) {
changedFiles.add(uri.toString())
// adds uri strings e.g. content://media/external/images/media/2281
contentUris.add(uri.toString())
}
FilesSyncHelper.startFilesSyncForAllFolders(
FilesSyncHelper.startAutoUploadImmediatelyWithContentUris(
syncedFolderProvider,
backgroundJobManager,
false,
changedFiles.toTypedArray()
contentUris.toTypedArray()
)
} else {
Log_OC.w(TAG, "cant startFilesSyncForAllFolders")
Log_OC.w(TAG, "cant startAutoUploadImmediatelyWithContentUris")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class AutoUploadWorker(
companion object {
const val TAG = "🔄📤" + "AutoUpload"
const val OVERRIDE_POWER_SAVING = "overridePowerSaving"
const val CHANGED_FILES = "changedFiles"
const val CONTENT_URIS = "content_uris"
const val SYNCED_FOLDER_ID = "syncedFolderId"
private const val CHANNEL_ID = NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD

Expand All @@ -89,13 +89,16 @@ class AutoUploadWorker(
val notification = createNotification(context.getString(R.string.upload_files))
updateForegroundInfo(notification)

val changedFiles = inputData.getStringArray(CHANGED_FILES)
/**
* Receives from [com.nextcloud.client.jobs.ContentObserverWork.checkAndTriggerAutoUpload]
*/
val contentUris = inputData.getStringArray(CONTENT_URIS)

if (canExitEarly(changedFiles, syncFolderId)) {
if (canExitEarly(contentUris, syncFolderId)) {
return Result.retry()
}

collectFileChangesFromContentObserverWork(changedFiles)
collectFileChangesFromContentObserverWork(contentUris)
updateNotification()
uploadFiles(syncedFolder)

Expand Down Expand Up @@ -158,7 +161,7 @@ class AutoUploadWorker(
}

@Suppress("ReturnCount")
private fun canExitEarly(changedFiles: Array<String>?, syncedFolderID: Long): Boolean {
private fun canExitEarly(contentUris: Array<String>?, syncedFolderID: Long): Boolean {
val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false)
if ((powerManagementService.isPowerSavingEnabled && !overridePowerSaving)) {
Log_OC.w(TAG, "⚡ Skipping: device is in power saving mode")
Expand All @@ -184,7 +187,7 @@ class AutoUploadWorker(
Log_OC.d(TAG, "currentTime: $currentTime")
Log_OC.d(TAG, "passedScanInterval: $passedScanInterval")

if (!passedScanInterval && changedFiles.isNullOrEmpty() && !overridePowerSaving) {
if (!passedScanInterval && contentUris.isNullOrEmpty() && !overridePowerSaving) {
Log_OC.w(
TAG,
"skipped since started before scan interval and nothing todo: " + syncedFolder.localPath
Expand All @@ -195,15 +198,25 @@ class AutoUploadWorker(
return false
}

/**
* Instead of scanning the entire local folder, optional content URIs can be passed to the worker
* to detect only the relevant changes.
*/
@Suppress("MagicNumber", "TooGenericExceptionCaught")
private suspend fun collectFileChangesFromContentObserverWork(changedFiles: Array<String>?) = try {
private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array<String>?) = try {
withContext(Dispatchers.IO) {
if (changedFiles.isNullOrEmpty()) {
// Check every file in synced folder for changes and update
// filesystemDataProvider database (potentially needs a long time)
if (contentUris.isNullOrEmpty()) {
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder)
} else {
FilesSyncHelper.insertChangedEntries(syncedFolder, changedFiles)
val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris)
if (!isContentUrisStored) {
Log_OC.w(
TAG,
"changed content uris not stored, fallback to insert all db entries to not lose files"
)

FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder)
}
}
syncedFolder.lastScanTimestampMs = System.currentTimeMillis()
syncedFolderProvider.updateSyncFolder(syncedFolder)
Expand Down
43 changes: 43 additions & 0 deletions app/src/main/java/com/nextcloud/utils/extensions/UriExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/com/owncloud/android/MainApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ public static void initSyncOperations(
}

if (!preferences.isAutoUploadInitialized()) {
FilesSyncHelper.startFilesSyncForAllFolders(syncedFolderProvider, backgroundJobManager,false, new String[]{});
FilesSyncHelper.startAutoUploadImmediately(syncedFolderProvider, backgroundJobManager, false);
preferences.setAutoUploadInit(true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,11 @@ public void setExcludeHidden(boolean excludeHidden) {
this.excludeHidden = excludeHidden;
}

public boolean containsTypedFile(String filePath){
public boolean containsTypedFile(File file,String filePath){
boolean isCorrectMediaType =
(getType() == MediaFolderType.IMAGE && MimeTypeUtil.isImage(new File(filePath))) ||
(getType() == MediaFolderType.VIDEO && MimeTypeUtil.isVideo(new File(filePath))) ||
getType() == MediaFolderType.CUSTOM;
(getType() == MediaFolderType.IMAGE && MimeTypeUtil.isImage(file)) ||
(getType() == MediaFolderType.VIDEO && MimeTypeUtil.isVideo(file)) ||
getType() == MediaFolderType.CUSTOM;
return filePath.contains(localPath) && isCorrectMediaType;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ class SyncedFoldersActivity :
}
}
if (syncedFolderDisplayItem.isEnabled) {
backgroundJobManager.startImmediateFilesSyncJob(syncedFolderDisplayItem, overridePowerSaving = false)
backgroundJobManager.startAutoUploadImmediately(syncedFolderDisplayItem, overridePowerSaving = false)
showBatteryOptimizationInfo()
}
}
Expand Down Expand Up @@ -725,7 +725,7 @@ class SyncedFoldersActivity :
// existing synced folder setup to be updated
syncedFolderProvider.updateSyncFolder(item)
if (item.isEnabled) {
backgroundJobManager.startImmediateFilesSyncJob(item, overridePowerSaving = false)
backgroundJobManager.startAutoUploadImmediately(item, overridePowerSaving = false)
} else {
val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id
val arbitraryDataProvider =
Expand All @@ -742,7 +742,7 @@ class SyncedFoldersActivity :
if (storedId != -1L) {
item.id = storedId
if (item.isEnabled) {
backgroundJobManager.startImmediateFilesSyncJob(item, overridePowerSaving = false)
backgroundJobManager.startAutoUploadImmediately(item, overridePowerSaving = false)
} else {
val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id
arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,9 @@ private void loadItems() {
}

private void refresh() {
FilesSyncHelper.startFilesSyncForAllFolders(syncedFolderProvider,
FilesSyncHelper.startAutoUploadImmediately(syncedFolderProvider,
backgroundJobManager,
true,
new String[]{});
true);

if (uploadsStorageManager.getFailedUploads().length > 0) {
new Thread(() -> {
Expand Down
95 changes: 59 additions & 36 deletions app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
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.upload.FileUploadHelper;
import com.nextcloud.client.jobs.upload.FileUploadWorker;
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;
Expand Down Expand Up @@ -212,47 +215,58 @@ public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder)
}
}

public static void insertChangedEntries(SyncedFolder syncedFolder,
String[] changedFiles) {
Log_OC.d(TAG, "insertChangedEntries, called. ID: " + syncedFolder.getId());
final ContentResolver contentResolver = MainApp.getAppContext().getContentResolver();
/**
* 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.
* <p>
* 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 changedFileURI : changedFiles){
String changedFile = getFileFromURI(changedFileURI);
if (syncedFolder.containsTypedFile(changedFile)){
File file = new File(changedFile);
if (!file.exists()) {
Log_OC.w(TAG, "syncedFolder contains not existing changed file: " + changedFile);
}
filesystemDataProvider.storeOrUpdateFileValue(changedFile,
file.lastModified(),file.isDirectory(),
syncedFolder);
} else {
Log_OC.w(TAG, "syncedFolder not contains typed file, changedFile: " + changedFile);
for (String contentUriString : contentUris) {
if (contentUriString == null) {
Log_OC.w(TAG, "null content uri string");
return false;
}
}
}

private static String getFileFromURI(String uri){
Log_OC.d(TAG, "getFileFromURI, URI: " + uri);
final Context context = MainApp.getAppContext();
Uri contentUri;
try {
contentUri = Uri.parse(contentUriString);
} catch (Exception e) {
Log_OC.e(TAG, "Invalid URI: " + contentUriString, e);
return false;
}

Cursor cursor;
int column_index_data;
String filePath = null;
String filePath = UriExtensionsKt.toFilePath(contentUri, context);
if (filePath == null) {
Log_OC.w(TAG, "File path is null");
return false;
}

String[] projection = {MediaStore.MediaColumns.DATA};
File file = new File(filePath);
if (!file.exists()) {
Log_OC.w(TAG, "syncedFolder contains not existing changed file: " + filePath);
return false;
}

cursor = context.getContentResolver().query(Uri.parse(uri), projection, null, null, null, null);
if (!syncedFolder.containsTypedFile(file, filePath)) {
Log_OC.w(TAG, "syncedFolder not contains typed file, changedFile: " + filePath);
return false;
}

if (cursor != null && cursor.moveToFirst()) {
column_index_data = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
filePath = cursor.getString(column_index_data);
cursor.close();
} else {
Log_OC.e(TAG, "cant get file from URI");
filesystemDataProvider.storeOrUpdateFileValue(filePath, file.lastModified(), file.isDirectory(), syncedFolder);
}
return filePath;

Log_OC.d(TAG, "changed content uris successfully stored");

return true;
}

private static void insertContentIntoDB(Uri uri, SyncedFolder syncedFolder,
Expand Down Expand Up @@ -345,11 +359,20 @@ public static void scheduleFilesSyncForAllFoldersIfNeeded(Context context, Synce
}
}

public static void startFilesSyncForAllFolders(SyncedFolderProvider syncedFolderProvider, BackgroundJobManager jobManager, boolean overridePowerSaving, String[] changedFiles) {
Log_OC.d(TAG, "startFilesSyncForAllFolders, called");
public static void startAutoUploadImmediatelyWithContentUris(SyncedFolderProvider syncedFolderProvider, BackgroundJobManager jobManager, boolean overridePowerSaving, String[] contentUris) {
Log_OC.d(TAG, "startAutoUploadImmediatelyWithContentUris");
for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
if (syncedFolder.isEnabled()) {
jobManager.startAutoUploadImmediately(syncedFolder, overridePowerSaving, contentUris);
}
}
}

public static void startAutoUploadImmediately(SyncedFolderProvider syncedFolderProvider, BackgroundJobManager jobManager, boolean overridePowerSaving) {
Log_OC.d(TAG, "startAutoUploadImmediately");
for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
if (syncedFolder.isEnabled()) {
jobManager.startImmediateFilesSyncJob(syncedFolder, overridePowerSaving,changedFiles);
jobManager.startAutoUploadImmediately(syncedFolder, overridePowerSaving, new String[]{});
}
}
}
Expand Down
Loading