From 49ee63ae14034d4d3f2fff3fac51207df794621b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Jul 2026 09:21:27 +0200 Subject: [PATCH 01/12] feat(auto-upload): ignore battery optimization with sync now Signed-off-by: alperozturk96 # Conflicts: # app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt --- .../client/jobs/BackgroundJobManagerImpl.kt | 6 +- .../jobs/autoUpload/AutoUploadWorker.kt | 10 ++- .../extensions/SyncedFolderExtensions.kt | 4 +- .../android/datamodel/SyncedFolder.java | 2 +- .../android/datamodel/SyncedFolderProvider.kt | 2 +- .../operations/UploadFileOperation.java | 20 +++-- .../ui/activity/SyncedFoldersActivity.kt | 14 +++- .../android/ui/activity/UploadListActivity.kt | 8 +- .../dialog/parcel/SyncedFolderParcelable.java | 2 +- .../owncloud/android/utils/FilesSyncHelper.kt | 11 ++- .../main/res/layout/upload_warning_card.xml | 25 +++++-- app/src/main/res/values/strings.xml | 3 + .../android/utils/FilesSyncHelperTest.kt | 74 +++++++++++++++++++ 13 files changed, 149 insertions(+), 32 deletions(-) create mode 100644 app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 4a28b1715dea..41212e0430e4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -508,9 +508,13 @@ internal class BackgroundJobManagerImpl( ) .build() + // an already scheduled run may still carry overridePowerSaving = false and would swallow an explicit + // user request, therefore replace it instead of keeping it + val policy = if (overridePowerSaving) ExistingWorkPolicy.REPLACE else ExistingWorkPolicy.KEEP + workManager.enqueueUniqueWork( JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID, - ExistingWorkPolicy.KEEP, + policy, request ) } 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 ba895036341f..1df94d167434 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 @@ -84,6 +84,9 @@ class AutoUploadWorker( private val fileUploadHelper = FileUploadHelper.instance() private val retryPolicy = UploadDelayPolicy() + private val overridePowerSaving: Boolean + get() = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) + @Suppress("ReturnCount") override suspend fun doWork(): Result { return try { @@ -185,7 +188,6 @@ class AutoUploadWorker( @Suppress("ReturnCount") private suspend fun canExitEarly(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") return true @@ -455,7 +457,11 @@ class AutoUploadWorker( upload.isWhileChargingOnly, true, FileDataStorageManager(user, context.contentResolver) - ) + ).apply { + if (overridePowerSaving) { + isIgnoringPowerSaveMode = true + } + } private fun sendUploadFinishEvent(operation: UploadFileOperation, result: RemoteOperationResult<*>) { fileUploadEventBroadcaster.sendUploadCompleted( 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 aaa112d41a8d..5041388ee99c 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt @@ -37,7 +37,7 @@ fun SyncedFolder.shouldSkipFile( } // If "upload existing files" is DISABLED, only upload files created after enabled time - if (!isExisting) { + if (!alsoUploadExistingFiles()) { if (creationTime != null) { if (creationTime < enabledTimestampMs) { Log_OC.d(TAG, "Skipping pre-existing file (creation < enabled): ${file.absolutePath}") @@ -149,7 +149,7 @@ fun SyncedFolder.getLog(): String { 📶 Wi-Fi only: $isWifiOnly 🔌 Charging only: $isChargingOnly - 📤 Upload existing files: $isExisting + 📤 Upload existing files: ${alsoUploadExistingFiles()} ⚙️ Upload action: $uploadAction 🧩 Name collision: $nameCollisionPolicy diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java index ef7d005f9e82..aa564d9b737c 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java @@ -201,7 +201,7 @@ public boolean isChargingOnly() { * * @return {@code true} if existing files should also be uploaded, {@code false} otherwise */ - public boolean isExisting() { + public boolean alsoUploadExistingFiles() { return this.existing; } diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.kt b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.kt index 1dae23982037..0e13d030531e 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.kt @@ -208,7 +208,7 @@ class SyncedFolderProvider( put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH, syncedFolder.remotePath) put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY, syncedFolder.isWifiOnly) put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY, syncedFolder.isChargingOnly) - put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXISTING, syncedFolder.isExisting) + put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXISTING, syncedFolder.alsoUploadExistingFiles()) put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED, syncedFolder.isEnabled) put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED_TIMESTAMP_MS, syncedFolder.enabledTimestampMs) put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE, syncedFolder.isSubfolderByDate) diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index 5923fdf6c3da..a330566539bf 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -143,7 +143,7 @@ public class UploadFileOperation extends SyncOperation { private volatile int mCreatedBy; private boolean mOnWifiOnly; private boolean mWhileChargingOnly; - private boolean mIgnoringPowerSaveMode; + private volatile boolean mIgnoringPowerSaveMode; private final boolean mDisableRetries; private volatile boolean mWasRenamed; @@ -294,6 +294,10 @@ public boolean isIgnoringPowerSaveMode() { return mIgnoringPowerSaveMode; } + public void setIgnoringPowerSaveMode(boolean ignoringPowerSaveMode) { + mIgnoringPowerSaveMode = ignoringPowerSaveMode; + } + public User getUser() { return user; } @@ -1002,38 +1006,38 @@ private RemoteOperationResult releaseLocksAndUnlockE2EFolder(FileLock fileLock, } // endregion - private RemoteOperationResult checkConditions(File originalFile) { - RemoteOperationResult remoteOperationResult = null; + private RemoteOperationResult checkConditions(File originalFile) { + RemoteOperationResult remoteOperationResult = null; // check that connectivity conditions are met and delays the upload otherwise Connectivity connectivity = connectivityService.getConnectivity(); if (mOnWifiOnly && (!connectivity.isWifi() || connectivity.isMetered())) { Log_OC.d(TAG, "Upload delayed until WiFi is available: " + getRemotePath()); - remoteOperationResult = new RemoteOperationResult(ResultCode.DELAYED_FOR_WIFI); + remoteOperationResult = new RemoteOperationResult<>(ResultCode.DELAYED_FOR_WIFI); } // check if charging conditions are met and delays the upload otherwise final BatteryStatus battery = powerManagementService.getBattery(); if (mWhileChargingOnly && !battery.isCharging()) { Log_OC.d(TAG, "Upload delayed until the device is charging: " + getRemotePath()); - remoteOperationResult = new RemoteOperationResult(ResultCode.DELAYED_FOR_CHARGING); + remoteOperationResult = new RemoteOperationResult<>(ResultCode.DELAYED_FOR_CHARGING); } // check that device is not in power save mode if (!mIgnoringPowerSaveMode && powerManagementService.isPowerSavingEnabled()) { Log_OC.d(TAG, "Upload delayed because device is in power save mode: " + getRemotePath()); - remoteOperationResult = new RemoteOperationResult(ResultCode.DELAYED_IN_POWER_SAVE_MODE); + remoteOperationResult = new RemoteOperationResult<>(ResultCode.DELAYED_IN_POWER_SAVE_MODE); } // check if the file continues existing before schedule the operation if (!originalFile.exists()) { Log_OC.d(TAG, mOriginalStoragePath + " does not exist anymore"); - remoteOperationResult = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND); + remoteOperationResult = new RemoteOperationResult<>(ResultCode.LOCAL_FILE_NOT_FOUND); } // check that internet is not behind walled garden if (!connectivityService.getConnectivity().isConnected() || connectivityService.isInternetWalled()) { - remoteOperationResult = new RemoteOperationResult(ResultCode.NO_NETWORK_CONNECTION); + remoteOperationResult = new RemoteOperationResult<>(ResultCode.NO_NETWORK_CONNECTION); } return remoteOperationResult; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 967c1995390d..03795c125851 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -167,7 +167,13 @@ class SyncedFoldersActivity : super.onCreate(savedInstanceState) binding = SyncedFoldersLayoutBinding.inflate(layoutInflater) setContentView(binding.root) - uploadWarningCard = UploadWarningCard(this, powerManagementService, viewThemeUtils) + uploadWarningCard = UploadWarningCard( + this, + powerManagementService, + syncedFolderProvider, + backgroundJobManager, + viewThemeUtils + ) if (intent != null && intent.extras != null) { val accountName = intent.extras!!.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT) val optionalUser = user @@ -405,7 +411,7 @@ class SyncedFoldersActivity : syncedFolder.remotePath, syncedFolder.isWifiOnly, syncedFolder.isChargingOnly, - syncedFolder.isExisting, + syncedFolder.alsoUploadExistingFiles(), syncedFolder.isSubfolderByDate, syncedFolder.account, syncedFolder.uploadAction, @@ -437,7 +443,7 @@ class SyncedFoldersActivity : syncedFolder.remotePath, syncedFolder.isWifiOnly, syncedFolder.isChargingOnly, - syncedFolder.isExisting, + syncedFolder.alsoUploadExistingFiles(), syncedFolder.isSubfolderByDate, syncedFolder.account, syncedFolder.uploadAction, @@ -852,7 +858,7 @@ class SyncedFoldersActivity : item.remotePath = remotePath item.isWifiOnly = wifiOnly item.isChargingOnly = chargingOnly - item.isExisting = existing + item.setExisting(existing) item.isSubfolderByDate = subfolderByDate item.uploadAction = uploadAction item.setNameCollisionPolicy(nameCollisionPolicy) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt index 33a63a157dc7..224598f77d3f 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt @@ -93,7 +93,13 @@ class UploadListActivity : binding = UploadListLayoutBinding.inflate(layoutInflater) val binding = binding!! setContentView(binding.getRoot()) - uploadWarningCard = UploadWarningCard(this, powerManagementService, viewThemeUtils) + uploadWarningCard = UploadWarningCard( + this, + powerManagementService, + syncedFolderProvider, + backgroundJobManager, + viewThemeUtils + ) swipeListRefreshLayout = binding.swipeContainingList // this activity has no file really bound, it's for multiple accounts at the same time; should no inherit diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java b/app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java index a8f68a9e0b4a..5e64943f31b4 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java @@ -45,7 +45,7 @@ public SyncedFolderParcelable(SyncedFolderDisplayItem syncedFolderDisplayItem, i remotePath = syncedFolderDisplayItem.getRemotePath(); wifiOnly = syncedFolderDisplayItem.isWifiOnly(); chargingOnly = syncedFolderDisplayItem.isChargingOnly(); - existing = syncedFolderDisplayItem.isExisting(); + existing = syncedFolderDisplayItem.alsoUploadExistingFiles(); enabled = syncedFolderDisplayItem.isEnabled(); subfolderByDate = syncedFolderDisplayItem.isSubfolderByDate(); type = syncedFolderDisplayItem.getType(); diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt index 9b18dea58a5b..f20eaeb84208 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -43,13 +43,12 @@ object FilesSyncHelper { provider: SyncedFolderProvider, manager: BackgroundJobManager, overridePowerSaving: Boolean - ) { + ): Int { Log_OC.d(TAG, "start auto upload worker for each enabled folder") - provider.syncedFolders.forEach { - if (it.isEnabled) { - manager.startAutoUpload(it, overridePowerSaving) - } - } + return provider.syncedFolders + .filter { it.isEnabled } + .onEach { manager.startAutoUpload(it, overridePowerSaving) } + .size } } diff --git a/app/src/main/res/layout/upload_warning_card.xml b/app/src/main/res/layout/upload_warning_card.xml index 4155c8bcaaaa..e5c0a670f548 100644 --- a/app/src/main/res/layout/upload_warning_card.xml +++ b/app/src/main/res/layout/upload_warning_card.xml @@ -58,15 +58,30 @@ tools:visibility="visible" /> - + android:orientation="horizontal"> + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b6cb7d99a11..4389d0c8ab99 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1500,6 +1500,9 @@ Nextcloud\'s background activity is limited by battery optimization. Open battery saver settings Allow background activity + Sync now + Auto upload started + No auto upload folder is enabled This will remove the folder and auto-upload configuration. Any unfinished uploads will be canceled. This folder is already included in the parent folder’s sync, which may cause duplicate uploads Sync anyway diff --git a/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt b/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt new file mode 100644 index 000000000000..0ee98ae602dc --- /dev/null +++ b/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.utils + +import com.nextcloud.client.jobs.BackgroundJobManager +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderProvider +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +class FilesSyncHelperTest { + + private val backgroundJobManager: BackgroundJobManager = mock() + + private fun syncedFolder(enabled: Boolean): SyncedFolder = mock { + on { isEnabled } doReturn enabled + } + + private fun provider(vararg folders: SyncedFolder): SyncedFolderProvider = mock { + on { syncedFolders } doReturn folders.toMutableList() + } + + @Test + fun `sync now starts only enabled folders and asks them to ignore power saving`() { + val enabled = syncedFolder(enabled = true) + val disabled = syncedFolder(enabled = false) + + val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + provider(enabled, disabled), + backgroundJobManager, + overridePowerSaving = true + ) + + assertEquals(1, startedFolderCount) + verify(backgroundJobManager).startAutoUpload(enabled, true) + verify(backgroundJobManager, never()).startAutoUpload(disabled, true) + } + + @Test + fun `scheduled runs keep the power saving check enabled`() { + val enabled = syncedFolder(enabled = true) + + val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + provider(enabled), + backgroundJobManager, + overridePowerSaving = false + ) + + assertEquals(1, startedFolderCount) + verify(backgroundJobManager).startAutoUpload(enabled, false) + } + + @Test + fun `no enabled folder reports nothing to sync`() { + val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + provider(syncedFolder(enabled = false)), + backgroundJobManager, + overridePowerSaving = true + ) + + assertEquals(0, startedFolderCount) + verify(backgroundJobManager, never()).startAutoUpload(any(), any()) + } +} From 950b7defb43c5cc2437bde2310760afda562fccb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Jul 2026 09:47:52 +0200 Subject: [PATCH 02/12] add override power saving option to the content observer work Signed-off-by: alperozturk96 --- .../client/jobs/BackgroundJobManager.kt | 5 ++- .../client/jobs/BackgroundJobManagerImpl.kt | 7 ++++- .../client/jobs/ContentObserverWork.kt | 8 +++-- .../java/com/owncloud/android/MainApp.java | 2 +- .../files/BootupBroadcastReceiver.java | 2 +- .../client/jobs/ContentObserverWorkTest.kt | 31 +++++++++++++++++++ 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index a859e5808d56..0a4aa4cdd719 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -35,8 +35,11 @@ interface BackgroundJobManager { * * This call is idempotent - there will be only one scheduled job * regardless of number of calls. + * + * @param overridePowerSaving lets an explicitly user triggered sync run even while the device is in power + * saving mode. The worker reschedules itself without the override afterwards. */ - fun scheduleContentObserverJob() + fun scheduleContentObserverJob(overridePowerSaving: Boolean = false) /** * Schedule periodic contacts backups job. Operating system will diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 41212e0430e4..ef01beddd55b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -464,7 +464,11 @@ internal class BackgroundJobManagerImpl( } @Suppress("MagicNumber") - override fun scheduleContentObserverJob() { + override fun scheduleContentObserverJob(overridePowerSaving: Boolean) { + val arguments = Data.Builder() + .putBoolean(ContentObserverWork.OVERRIDE_POWER_SAVING, overridePowerSaving) + .build() + val constrains = Constraints.Builder() .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) @@ -477,6 +481,7 @@ internal class BackgroundJobManagerImpl( val request = oneTimeRequestBuilder(ContentObserverWork::class, JOB_CONTENT_OBSERVER) .setConstraints(constrains) + .setInputData(arguments) .build() workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.REPLACE, request) diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt index 214b85239b43..6e0f91446a63 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt @@ -36,8 +36,12 @@ class ContentObserverWork( companion object { private const val TAG = "🔍" + "ContentObserverWork" + const val OVERRIDE_POWER_SAVING = "overridePowerSaving" } + private val overridePowerSaving: Boolean + get() = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { val workerName = BackgroundJobManagerImpl.formatClassTag(this@ContentObserverWork::class) backgroundJobManager.logStartOfWorker(workerName) @@ -72,7 +76,7 @@ class ContentObserverWork( } private suspend fun checkAndTriggerAutoUpload() = withContext(Dispatchers.IO) { - if (powerManagementService.isPowerSavingEnabled) { + if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) { Log_OC.w(TAG, "⚡ Power saving mode active — skipping file sync.") return@withContext } @@ -116,7 +120,7 @@ class ContentObserverWork( FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( syncedFolderProvider, backgroundJobManager, - false + overridePowerSaving ) Log_OC.d(TAG, "✅ auto upload triggered successfully for ${contentUris.size} file(s).") } catch (e: Exception) { diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index f08bde45fee9..d9971e05ae4c 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -335,7 +335,7 @@ public void onCreate() { } Log_OC.d(TAG, "scheduleContentObserverJob, called"); - backgroundJobManager.scheduleContentObserverJob(); + backgroundJobManager.scheduleContentObserverJob(false); initSyncOperations(this, preferences, diff --git a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java index 8eb62f17a9c9..9bb3e0820d07 100644 --- a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java +++ b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java @@ -71,7 +71,7 @@ public void onReceive(Context context, Intent intent) { viewThemeUtils, walledCheckCache); Log_OC.d(TAG, "scheduleContentObserverJob, called"); - backgroundJobManager.scheduleContentObserverJob(); + backgroundJobManager.scheduleContentObserverJob(false); MainApp.initContactsBackup(accountManager, backgroundJobManager); } else { Log_OC.d(TAG, "Getting wrong intent: " + intent.getAction()); diff --git a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt index 53846287b2db..d62650c281b5 100644 --- a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt +++ b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt @@ -10,6 +10,7 @@ package com.nextcloud.client.jobs import android.content.Context import android.net.Uri +import androidx.work.Data import androidx.work.WorkerParameters import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.device.PowerManagementService @@ -27,6 +28,7 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -70,6 +72,7 @@ class ContentObserverWorkTest { ) val uri: Uri = Mockito.mock(Uri::class.java) whenever(params.triggeredContentUris).thenReturn(listOf(uri)) + whenever(params.inputData).thenReturn(Data.EMPTY) } @After @@ -94,6 +97,34 @@ class ContentObserverWorkTest { } } + @Test + fun power_saving_stops_a_regular_run_before_folders_are_read() { + runBlocking { + whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) + + worker.doWork() + + verify(folderProvider, never()).countEnabledSyncedFolders() + } + } + + @Test + fun power_saving_does_not_stop_a_run_the_user_asked_for() { + runBlocking { + whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) + whenever(params.inputData).thenReturn( + Data.Builder() + .putBoolean(ContentObserverWork.OVERRIDE_POWER_SAVING, true) + .build() + ) + whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(0) + + worker.doWork() + + verify(folderProvider).countEnabledSyncedFolders() + } + } + @Test @Ignore("TODO: needs further refactoring") fun sync_is_triggered() { From 0837f9308b19f5f56bcfdc5ecd3ffb9365c413d6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Jul 2026 10:05:11 +0200 Subject: [PATCH 03/12] fix infinite loading indicator Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/activity/UploadListActivity.kt | 2 ++ .../android/ui/adapter/uploadList/UploadListAdapter.kt | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt index 224598f77d3f..6e5996eb3800 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt @@ -185,6 +185,8 @@ class UploadListActivity : accountManager, powerManagementService ) + + loadItems() } override fun onStart() { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt index e1f34a37e5f9..9bfb9f6baaa0 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt @@ -555,7 +555,10 @@ class UploadListAdapter( fun loadUploadItemsFromDb(onCompleted: Runnable = {}) { val optionalUser = activity.user val optionalCapabilities = activity.capabilities - if (optionalUser.isEmpty || optionalCapabilities.isEmpty) return + if (optionalUser.isEmpty || optionalCapabilities.isEmpty) { + onCompleted.run() + return + } val accountName = optionalUser.get().accountName val capabilities = optionalCapabilities.get() From 873282de67d7ff96efcef0ed796bc918e35e071b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 12 Aug 2026 08:59:16 +0200 Subject: [PATCH 04/12] wip Signed-off-by: alperozturk96 --- .../ui/component/UploadWarningCard.kt | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index b0af6ec9abd5..3a3056c043b5 100644 --- a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -17,13 +17,20 @@ import android.provider.Settings import android.view.View import androidx.core.net.toUri import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.utils.extensions.setVisibleIf +import com.owncloud.android.R import com.owncloud.android.databinding.UploadWarningCardBinding +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.theme.ViewThemeUtils class UploadWarningCard( private val context: Context, private val powerManagementService: PowerManagementService, + private val syncedFolderProvider: SyncedFolderProvider, + private val backgroundJobManager: BackgroundJobManager, private val viewThemeUtils: ViewThemeUtils ) { fun bind(binding: UploadWarningCardBinding) { @@ -33,12 +40,19 @@ class UploadWarningCard( binding.root.setVisibleIf(isBatterySaver || !isIgnoringOptimization) if (isBatterySaver) { - viewThemeUtils.material.themeCardView(binding.batterySaverLayout) + viewThemeUtils.material.run { + themeCardView(binding.batterySaverLayout) + colorMaterialButtonPrimaryBorderless(binding.batterySaverButton) + colorMaterialTextButton(binding.syncNowButton) + } + binding.batterySaverLayout.visibility = View.VISIBLE binding.batterySaverButton.setOnClickListener { openBatterySaverPage() } - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.batterySaverButton) + binding.syncNowButton.setOnClickListener { + startAutoUploadViaIgnoringBatteryOptimization(it) + } } else { binding.batterySaverLayout.visibility = View.GONE } @@ -94,4 +108,22 @@ class UploadWarningCard( intent.data = "package:${context.packageName}".toUri() context.startActivity(intent) } + + private fun startAutoUploadViaIgnoringBatteryOptimization(view: View) { + val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + syncedFolderProvider, + backgroundJobManager, + overridePowerSaving = true + ) + + backgroundJobManager.scheduleContentObserverJob(overridePowerSaving = true) + + val message = if (startedFolderCount > 0) { + R.string.auto_upload_sync_now_started + } else { + R.string.auto_upload_sync_now_no_folder + } + + DisplayUtils.showSnackMessage(view, message) + } } From 017d9a8b90f67c106fd2dd0e9df219bcfea01c2a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 19 Aug 2026 15:57:11 +0200 Subject: [PATCH 05/12] fix button alignment Signed-off-by: alperozturk96 --- app/src/main/res/layout/upload_warning_card.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/upload_warning_card.xml b/app/src/main/res/layout/upload_warning_card.xml index e5c0a670f548..1e37f351367b 100644 --- a/app/src/main/res/layout/upload_warning_card.xml +++ b/app/src/main/res/layout/upload_warning_card.xml @@ -59,9 +59,9 @@ @@ -69,7 +69,7 @@ android:id="@+id/battery_saver_button" style="@style/Widget.MaterialComponents.Button.TextButton" android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_height="match_parent" android:textAllCaps="false" android:text="@string/auto_upload_battery_saver_action" /> @@ -77,8 +77,7 @@ android:id="@+id/sync_now_button" style="@style/Widget.MaterialComponents.Button.TextButton" android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/standard_half_margin" + android:layout_height="match_parent" android:textAllCaps="false" android:text="@string/auto_upload_sync_now_action" /> From 2b76712b9da40c38ed1099b458ce90fb4fc2f3b9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 13:52:32 +0200 Subject: [PATCH 06/12] wip Signed-off-by: alperozturk96 --- .../main/java/com/nextcloud/ui/component/UploadWarningCard.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index 3a3056c043b5..a4fd9e75ae95 100644 --- a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -43,7 +43,7 @@ class UploadWarningCard( viewThemeUtils.material.run { themeCardView(binding.batterySaverLayout) colorMaterialButtonPrimaryBorderless(binding.batterySaverButton) - colorMaterialTextButton(binding.syncNowButton) + colorMaterialButtonPrimaryBorderless(binding.syncNowButton) } binding.batterySaverLayout.visibility = View.VISIBLE From e9adcdca0c1da0d5eb40c7869331c23f9907c4f9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 14:19:57 +0200 Subject: [PATCH 07/12] add debounce Signed-off-by: alperozturk96 --- .../ui/component/UploadWarningCard.kt | 24 +++++++++++++------ app/src/main/res/values/strings.xml | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index a4fd9e75ae95..bd670daf8d12 100644 --- a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -13,6 +13,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.PowerManager +import android.os.SystemClock import android.provider.Settings import android.view.View import androidx.core.net.toUri @@ -25,6 +26,7 @@ import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.theme.ViewThemeUtils +import java.util.concurrent.TimeUnit class UploadWarningCard( private val context: Context, @@ -110,7 +112,13 @@ class UploadWarningCard( } private fun startAutoUploadViaIgnoringBatteryOptimization(view: View) { - val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + val now = SystemClock.elapsedRealtime() + if (now - lastSyncNowTime < SYNC_NOW_COOLDOWN_MS) { + DisplayUtils.showSnackMessage(view, R.string.auto_upload_sync_now_min_warning) + return + } + + FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( syncedFolderProvider, backgroundJobManager, overridePowerSaving = true @@ -118,12 +126,14 @@ class UploadWarningCard( backgroundJobManager.scheduleContentObserverJob(overridePowerSaving = true) - val message = if (startedFolderCount > 0) { - R.string.auto_upload_sync_now_started - } else { - R.string.auto_upload_sync_now_no_folder - } + lastSyncNowTime = now + + DisplayUtils.showSnackMessage(view, R.string.auto_upload_sync_now_started) + } - DisplayUtils.showSnackMessage(view, message) + companion object { + private val SYNC_NOW_COOLDOWN_MS = TimeUnit.MINUTES.toMillis(SYNC_NOW_COOLDOWN_MINUTES) + private const val SYNC_NOW_COOLDOWN_MINUTES = 5L + private var lastSyncNowTime = 0L } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4389d0c8ab99..261f29da5afb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1502,7 +1502,7 @@ Allow background activity Sync now Auto upload started - No auto upload folder is enabled + Auto upload was started recently, please try again later. This will remove the folder and auto-upload configuration. Any unfinished uploads will be canceled. This folder is already included in the parent folder’s sync, which may cause duplicate uploads Sync anyway From eb9df06870d1b097c60c9861ebed3f03d3de9cea Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 09:06:06 +0200 Subject: [PATCH 08/12] instead of using random minutes to prevent sync now use worker Signed-off-by: alperozturk96 --- .../client/jobs/BackgroundJobManager.kt | 2 +- .../client/jobs/BackgroundJobManagerImpl.kt | 14 ++++----- .../ui/component/UploadWarningCard.kt | 31 +++++++------------ .../owncloud/android/utils/FilesSyncHelper.kt | 17 ++++++++++ app/src/main/res/values/strings.xml | 1 - 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 0a4aa4cdd719..22dee677c813 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -143,7 +143,7 @@ interface BackgroundJobManager { fun getFileUploads(user: User): LiveData> fun cancelFilesUploadJob(user: User) fun isStartFileUploadJobScheduled(accountName: String): Boolean - + fun getAutoUploadTag(syncedFolderID: Long): String fun cancelFilesDownloadJob(accountName: String, fileId: Long) @Suppress("LongParameterList") diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index ef01beddd55b..3ca537d7250c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -487,6 +487,10 @@ internal class BackgroundJobManagerImpl( workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.REPLACE, request) } + override fun getAutoUploadTag(syncedFolderID: Long): String { + return JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID + } + override fun startAutoUpload(syncedFolder: SyncedFolder, overridePowerSaving: Boolean) { val syncedFolderID = syncedFolder.id @@ -502,7 +506,7 @@ internal class BackgroundJobManagerImpl( val request = oneTimeRequestBuilder( jobClass = AutoUploadWorker::class, - jobName = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID + jobName = getAutoUploadTag(syncedFolderID) ) .setInputData(arguments) .setConstraints(constraints) @@ -513,13 +517,9 @@ internal class BackgroundJobManagerImpl( ) .build() - // an already scheduled run may still carry overridePowerSaving = false and would swallow an explicit - // user request, therefore replace it instead of keeping it - val policy = if (overridePowerSaving) ExistingWorkPolicy.REPLACE else ExistingWorkPolicy.KEEP - workManager.enqueueUniqueWork( - JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID, - policy, + getAutoUploadTag(syncedFolderID), + ExistingWorkPolicy.KEEP, request ) } diff --git a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index bd670daf8d12..ab622f10edf0 100644 --- a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -13,12 +13,14 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.PowerManager -import android.os.SystemClock import android.provider.Settings import android.view.View import androidx.core.net.toUri +import androidx.work.WorkManager import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.BackgroundJobManagerImpl.Companion.JOB_CONTENT_OBSERVER +import com.nextcloud.utils.extensions.isWorkScheduled import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.UploadWarningCardBinding @@ -26,7 +28,6 @@ import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.theme.ViewThemeUtils -import java.util.concurrent.TimeUnit class UploadWarningCard( private val context: Context, @@ -112,28 +113,20 @@ class UploadWarningCard( } private fun startAutoUploadViaIgnoringBatteryOptimization(view: View) { - val now = SystemClock.elapsedRealtime() - if (now - lastSyncNowTime < SYNC_NOW_COOLDOWN_MS) { - DisplayUtils.showSnackMessage(view, R.string.auto_upload_sync_now_min_warning) - return - } - - FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + val startedAutoUploadSize = FilesSyncHelper.startAutoUploadForEnabledAndNotRunningSyncedFolders( + context, syncedFolderProvider, backgroundJobManager, overridePowerSaving = true ) - backgroundJobManager.scheduleContentObserverJob(overridePowerSaving = true) - - lastSyncNowTime = now - - DisplayUtils.showSnackMessage(view, R.string.auto_upload_sync_now_started) - } + val isContentObserverScheduled = WorkManager.getInstance(context).isWorkScheduled(JOB_CONTENT_OBSERVER) + if (!isContentObserverScheduled) { + backgroundJobManager.scheduleContentObserverJob(overridePowerSaving = true) + } - companion object { - private val SYNC_NOW_COOLDOWN_MS = TimeUnit.MINUTES.toMillis(SYNC_NOW_COOLDOWN_MINUTES) - private const val SYNC_NOW_COOLDOWN_MINUTES = 5L - private var lastSyncNowTime = 0L + if (startedAutoUploadSize > 0 || !isContentObserverScheduled) { + DisplayUtils.showSnackMessage(view, R.string.auto_upload_sync_now_started) + } } } diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt index f20eaeb84208..2f1e2a093413 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -9,11 +9,14 @@ */ package com.owncloud.android.utils +import android.content.Context +import androidx.work.WorkManager import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadHelper.Companion.instance import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.utils.extensions.isWorkScheduled import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.lib.common.utils.Log_OC @@ -51,4 +54,18 @@ object FilesSyncHelper { .onEach { manager.startAutoUpload(it, overridePowerSaving) } .size } + + fun startAutoUploadForEnabledAndNotRunningSyncedFolders( + context: Context, + provider: SyncedFolderProvider, + manager: BackgroundJobManager, + overridePowerSaving: Boolean + ): Int { + val workManager = WorkManager.getInstance(context) + + return provider.syncedFolders + .filter { it.isEnabled && !workManager.isWorkScheduled(manager.getAutoUploadTag(it.id)) } + .onEach { manager.startAutoUpload(it, overridePowerSaving) } + .size + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 261f29da5afb..aa4b6463197f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1502,7 +1502,6 @@ Allow background activity Sync now Auto upload started - Auto upload was started recently, please try again later. This will remove the folder and auto-upload configuration. Any unfinished uploads will be canceled. This folder is already included in the parent folder’s sync, which may cause duplicate uploads Sync anyway From cbacc0800a1921f7026320ea1f8bdefca30d80a8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 09:07:16 +0200 Subject: [PATCH 09/12] instead of using random minutes to prevent sync now use worker Signed-off-by: alperozturk96 --- .../java/com/nextcloud/ui/component/UploadWarningCard.kt | 8 ++++++-- app/src/main/res/values/strings.xml | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index ab622f10edf0..ca316e032603 100644 --- a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -125,8 +125,12 @@ class UploadWarningCard( backgroundJobManager.scheduleContentObserverJob(overridePowerSaving = true) } - if (startedAutoUploadSize > 0 || !isContentObserverScheduled) { - DisplayUtils.showSnackMessage(view, R.string.auto_upload_sync_now_started) + val messageId = if (startedAutoUploadSize > 0 || !isContentObserverScheduled) { + R.string.auto_upload_sync_now_started + } else { + R.string.auto_upload_sync_now_running } + + DisplayUtils.showSnackMessage(view, messageId) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa4b6463197f..752dc5ac2577 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1502,6 +1502,7 @@ Allow background activity Sync now Auto upload started + Auto upload already running This will remove the folder and auto-upload configuration. Any unfinished uploads will be canceled. This folder is already included in the parent folder’s sync, which may cause duplicate uploads Sync anyway From 53a028b1716b5ac3b480c0c6f2b1dd126550f696 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 09:26:35 +0200 Subject: [PATCH 10/12] instead of using random minutes to prevent sync now use worker Signed-off-by: alperozturk96 --- .../client/jobs/BackgroundJobManager.kt | 13 +++-- .../client/jobs/BackgroundJobManagerImpl.kt | 41 +++++++++------ .../client/jobs/ContentObserverWork.kt | 8 +-- .../autoUpload/AutoUploadRequestResult.kt | 14 +++++ .../ui/component/UploadWarningCard.kt | 46 ++++++++-------- .../utils/extensions/WorkManagerExtensions.kt | 23 +++++--- .../java/com/owncloud/android/MainApp.java | 2 +- .../files/BootupBroadcastReceiver.java | 2 +- .../ui/activity/SyncedFoldersActivity.kt | 1 + .../android/ui/activity/UploadListActivity.kt | 1 + .../owncloud/android/utils/FilesSyncHelper.kt | 37 ++++++++----- app/src/main/res/values/strings.xml | 1 + .../client/jobs/ContentObserverWorkTest.kt | 21 +------- .../android/utils/FilesSyncHelperTest.kt | 52 ++++++++++++------- 14 files changed, 152 insertions(+), 110 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 22dee677c813..044f4fbb79c3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -35,11 +35,10 @@ interface BackgroundJobManager { * * This call is idempotent - there will be only one scheduled job * regardless of number of calls. - * - * @param overridePowerSaving lets an explicitly user triggered sync run even while the device is in power - * saving mode. The worker reschedules itself without the override afterwards. */ - fun scheduleContentObserverJob(overridePowerSaving: Boolean = false) + fun scheduleContentObserverJob() + + fun isContentObserverJobScheduled(): Boolean /** * Schedule periodic contacts backups job. Operating system will @@ -123,6 +122,10 @@ interface BackgroundJobManager { fun startImmediateFilesExportJob(files: Collection): LiveData + /** + * @param overridePowerSaving uploads even while the device is in power saving mode. Such a run replaces an + * already scheduled run of the same folder, because that one would otherwise stop on the power saving check. + */ fun startAutoUpload(syncedFolder: SyncedFolder, overridePowerSaving: Boolean = false) fun cancelTwoWaySyncJob() @@ -143,7 +146,7 @@ interface BackgroundJobManager { fun getFileUploads(user: User): LiveData> fun cancelFilesUploadJob(user: User) fun isStartFileUploadJobScheduled(accountName: String): Boolean - fun getAutoUploadTag(syncedFolderID: Long): String + fun isAutoUploadIgnoringPowerSavingScheduled(syncedFolderID: Long): Boolean fun cancelFilesDownloadJob(accountName: String, fileId: Long) @Suppress("LongParameterList") diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 3ca537d7250c..2a3949f414b0 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -37,6 +37,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.jobs.worker.WorkerFilesPayload import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.extensions.isUniqueWorkScheduled import com.nextcloud.utils.extensions.isWorkScheduled import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SyncedFolder @@ -106,6 +107,8 @@ internal class BackgroundJobManagerImpl( const val JOB_TEST = "test_job" + private const val TAG_SUFFIX_IGNORE_POWER_SAVING = "ignore_power_saving" + const val TAG_PREFIX_NAME = "name" const val TAG_PREFIX_USER = "user" const val TAG_PREFIX_CLASS = "class" @@ -464,11 +467,7 @@ internal class BackgroundJobManagerImpl( } @Suppress("MagicNumber") - override fun scheduleContentObserverJob(overridePowerSaving: Boolean) { - val arguments = Data.Builder() - .putBoolean(ContentObserverWork.OVERRIDE_POWER_SAVING, overridePowerSaving) - .build() - + override fun scheduleContentObserverJob() { val constrains = Constraints.Builder() .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) @@ -481,15 +480,20 @@ internal class BackgroundJobManagerImpl( val request = oneTimeRequestBuilder(ContentObserverWork::class, JOB_CONTENT_OBSERVER) .setConstraints(constrains) - .setInputData(arguments) .build() workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.REPLACE, request) } - override fun getAutoUploadTag(syncedFolderID: Long): String { - return JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID - } + override fun isContentObserverJobScheduled(): Boolean = workManager.isUniqueWorkScheduled(JOB_CONTENT_OBSERVER) + + private fun autoUploadWorkName(syncedFolderID: Long): String = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID + + private fun autoUploadIgnorePowerSavingTag(syncedFolderID: Long): String = + autoUploadWorkName(syncedFolderID) + "_" + TAG_SUFFIX_IGNORE_POWER_SAVING + + override fun isAutoUploadIgnoringPowerSavingScheduled(syncedFolderID: Long): Boolean = + workManager.isWorkScheduled(autoUploadIgnorePowerSavingTag(syncedFolderID)) override fun startAutoUpload(syncedFolder: SyncedFolder, overridePowerSaving: Boolean) { val syncedFolderID = syncedFolder.id @@ -504,9 +508,9 @@ internal class BackgroundJobManagerImpl( .setRequiresCharging(syncedFolder.isChargingOnly) .build() - val request = oneTimeRequestBuilder( + val requestBuilder = oneTimeRequestBuilder( jobClass = AutoUploadWorker::class, - jobName = getAutoUploadTag(syncedFolderID) + jobName = autoUploadWorkName(syncedFolderID) ) .setInputData(arguments) .setConstraints(constraints) @@ -515,12 +519,19 @@ internal class BackgroundJobManagerImpl( DEFAULT_BACKOFF_CRITERIA_DELAY_SEC, TimeUnit.SECONDS ) - .build() + + if (overridePowerSaving) { + requestBuilder.addTag(autoUploadIgnorePowerSavingTag(syncedFolderID)) + } + + // a scheduled run still carries its own overridePowerSaving flag, so keeping it would swallow the + // explicit user request and stop on the power saving check + val policy = if (overridePowerSaving) ExistingWorkPolicy.REPLACE else ExistingWorkPolicy.KEEP workManager.enqueueUniqueWork( - getAutoUploadTag(syncedFolderID), - ExistingWorkPolicy.KEEP, - request + autoUploadWorkName(syncedFolderID), + policy, + requestBuilder.build() ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt index 6e0f91446a63..214b85239b43 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt @@ -36,12 +36,8 @@ class ContentObserverWork( companion object { private const val TAG = "🔍" + "ContentObserverWork" - const val OVERRIDE_POWER_SAVING = "overridePowerSaving" } - private val overridePowerSaving: Boolean - get() = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) - override suspend fun doWork(): Result = withContext(Dispatchers.IO) { val workerName = BackgroundJobManagerImpl.formatClassTag(this@ContentObserverWork::class) backgroundJobManager.logStartOfWorker(workerName) @@ -76,7 +72,7 @@ class ContentObserverWork( } private suspend fun checkAndTriggerAutoUpload() = withContext(Dispatchers.IO) { - if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) { + if (powerManagementService.isPowerSavingEnabled) { Log_OC.w(TAG, "⚡ Power saving mode active — skipping file sync.") return@withContext } @@ -120,7 +116,7 @@ class ContentObserverWork( FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( syncedFolderProvider, backgroundJobManager, - overridePowerSaving + false ) Log_OC.d(TAG, "✅ auto upload triggered successfully for ${contentUris.size} file(s).") } catch (e: Exception) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt new file mode 100644 index 000000000000..57e55568f8b7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +enum class AutoUploadRequestResult { + STARTED, + ALREADY_RUNNING, + NO_ENABLED_FOLDER +} diff --git a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index ca316e032603..cfc15e568f22 100644 --- a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -15,12 +15,11 @@ import android.content.IntentFilter import android.os.PowerManager import android.provider.Settings import android.view.View +import androidx.annotation.StringRes import androidx.core.net.toUri -import androidx.work.WorkManager import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager -import com.nextcloud.client.jobs.BackgroundJobManagerImpl.Companion.JOB_CONTENT_OBSERVER -import com.nextcloud.utils.extensions.isWorkScheduled +import com.nextcloud.client.jobs.autoUpload.AutoUploadRequestResult import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.UploadWarningCardBinding @@ -28,12 +27,18 @@ import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +@Suppress("LongParameterList") class UploadWarningCard( private val context: Context, private val powerManagementService: PowerManagementService, private val syncedFolderProvider: SyncedFolderProvider, private val backgroundJobManager: BackgroundJobManager, + private val scope: CoroutineScope, private val viewThemeUtils: ViewThemeUtils ) { fun bind(binding: UploadWarningCardBinding) { @@ -54,7 +59,7 @@ class UploadWarningCard( openBatterySaverPage() } binding.syncNowButton.setOnClickListener { - startAutoUploadViaIgnoringBatteryOptimization(it) + startAutoUploadIgnoringBatterySaver(it) } } else { binding.batterySaverLayout.visibility = View.GONE @@ -112,25 +117,24 @@ class UploadWarningCard( context.startActivity(intent) } - private fun startAutoUploadViaIgnoringBatteryOptimization(view: View) { - val startedAutoUploadSize = FilesSyncHelper.startAutoUploadForEnabledAndNotRunningSyncedFolders( - context, - syncedFolderProvider, - backgroundJobManager, - overridePowerSaving = true - ) - - val isContentObserverScheduled = WorkManager.getInstance(context).isWorkScheduled(JOB_CONTENT_OBSERVER) - if (!isContentObserverScheduled) { - backgroundJobManager.scheduleContentObserverJob(overridePowerSaving = true) - } + private fun startAutoUploadIgnoringBatterySaver(view: View) { + scope.launch { + val result = withContext(Dispatchers.IO) { + if (!backgroundJobManager.isContentObserverJobScheduled()) { + backgroundJobManager.scheduleContentObserverJob() + } - val messageId = if (startedAutoUploadSize > 0 || !isContentObserverScheduled) { - R.string.auto_upload_sync_now_started - } else { - R.string.auto_upload_sync_now_running + FilesSyncHelper.startAutoUploadIgnoringPowerSaving(syncedFolderProvider, backgroundJobManager) + } + + DisplayUtils.showSnackMessage(view, result.messageId()) } + } - DisplayUtils.showSnackMessage(view, messageId) + @StringRes + private fun AutoUploadRequestResult.messageId(): Int = when (this) { + AutoUploadRequestResult.STARTED -> R.string.auto_upload_sync_now_started + AutoUploadRequestResult.ALREADY_RUNNING -> R.string.auto_upload_sync_now_running + AutoUploadRequestResult.NO_ENABLED_FOLDER -> R.string.auto_upload_sync_now_no_folder } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt index 9a3a8b69c7b2..1e6df23941a0 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt @@ -15,21 +15,28 @@ import java.util.concurrent.ExecutionException private const val TAG = "WorkManager" -fun WorkManager.isWorkRunning(tag: String): Boolean = checkWork(tag, listOf(WorkInfo.State.RUNNING)) +private val RUNNING_STATES = listOf(WorkInfo.State.RUNNING) +private val PENDING_STATES = listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED) -fun WorkManager.isWorkScheduled(tag: String): Boolean = - checkWork(tag, listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED)) +fun WorkManager.isWorkRunning(tag: String): Boolean = getWorkInfosByTag(tag).hasWorkIn(RUNNING_STATES) -private fun WorkManager.checkWork(tag: String, stateConditions: List): Boolean { - val statuses: ListenableFuture> = getWorkInfosByTag(tag) +fun WorkManager.isWorkScheduled(tag: String): Boolean = getWorkInfosByTag(tag).hasWorkIn(PENDING_STATES) + +/** + * Unique work names are not tags, [getWorkInfosByTag] never matches them. + */ +fun WorkManager.isUniqueWorkScheduled(uniqueWorkName: String): Boolean = + getWorkInfosForUniqueWork(uniqueWorkName).hasWorkIn(PENDING_STATES) + +private fun ListenableFuture>.hasWorkIn(stateConditions: List): Boolean { var workInfoList: List = emptyList() try { - workInfoList = statuses.get() + workInfoList = get() } catch (e: ExecutionException) { - Log_OC.d(TAG, "ExecutionException in checkWork: $e") + Log_OC.d(TAG, "ExecutionException in hasWorkIn: $e") } catch (e: InterruptedException) { - Log_OC.d(TAG, "InterruptedException in checkWork: $e") + Log_OC.d(TAG, "InterruptedException in hasWorkIn: $e") } return workInfoList.any { workInfo -> stateConditions.contains(workInfo.state) } diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index d9971e05ae4c..f08bde45fee9 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -335,7 +335,7 @@ public void onCreate() { } Log_OC.d(TAG, "scheduleContentObserverJob, called"); - backgroundJobManager.scheduleContentObserverJob(false); + backgroundJobManager.scheduleContentObserverJob(); initSyncOperations(this, preferences, diff --git a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java index 9bb3e0820d07..8eb62f17a9c9 100644 --- a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java +++ b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java @@ -71,7 +71,7 @@ public void onReceive(Context context, Intent intent) { viewThemeUtils, walledCheckCache); Log_OC.d(TAG, "scheduleContentObserverJob, called"); - backgroundJobManager.scheduleContentObserverJob(false); + backgroundJobManager.scheduleContentObserverJob(); MainApp.initContactsBackup(accountManager, backgroundJobManager); } else { Log_OC.d(TAG, "Getting wrong intent: " + intent.getAction()); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 03795c125851..dd66ce23c604 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -172,6 +172,7 @@ class SyncedFoldersActivity : powerManagementService, syncedFolderProvider, backgroundJobManager, + lifecycleScope, viewThemeUtils ) if (intent != null && intent.extras != null) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt index 6e5996eb3800..73734ad91819 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt @@ -98,6 +98,7 @@ class UploadListActivity : powerManagementService, syncedFolderProvider, backgroundJobManager, + lifecycleScope, viewThemeUtils ) swipeListRefreshLayout = binding.swipeContainingList diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt index 2f1e2a093413..9d40d2720242 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -9,14 +9,12 @@ */ package com.owncloud.android.utils -import android.content.Context -import androidx.work.WorkManager import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.autoUpload.AutoUploadRequestResult import com.nextcloud.client.jobs.upload.FileUploadHelper.Companion.instance import com.nextcloud.client.network.ConnectivityService -import com.nextcloud.utils.extensions.isWorkScheduled import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.lib.common.utils.Log_OC @@ -55,17 +53,30 @@ object FilesSyncHelper { .size } - fun startAutoUploadForEnabledAndNotRunningSyncedFolders( - context: Context, + /** + * Folders whose upload already ignores power saving are left alone, so pressing sync now twice does not + * cancel and restart an upload that is running for the same reason. + */ + fun startAutoUploadIgnoringPowerSaving( provider: SyncedFolderProvider, - manager: BackgroundJobManager, - overridePowerSaving: Boolean - ): Int { - val workManager = WorkManager.getInstance(context) + manager: BackgroundJobManager + ): AutoUploadRequestResult { + val enabledFolders = provider.syncedFolders.filter { it.isEnabled } + if (enabledFolders.isEmpty()) { + Log_OC.d(TAG, "no enabled synced folder to start") + return AutoUploadRequestResult.NO_ENABLED_FOLDER + } - return provider.syncedFolders - .filter { it.isEnabled && !workManager.isWorkScheduled(manager.getAutoUploadTag(it.id)) } - .onEach { manager.startAutoUpload(it, overridePowerSaving) } - .size + val startedFolders = enabledFolders + .filterNot { manager.isAutoUploadIgnoringPowerSavingScheduled(it.id) } + .onEach { manager.startAutoUpload(it, overridePowerSaving = true) } + + Log_OC.d(TAG, "start auto upload ignoring power saving for ${startedFolders.size} folder(s)") + + return if (startedFolders.isEmpty()) { + AutoUploadRequestResult.ALREADY_RUNNING + } else { + AutoUploadRequestResult.STARTED + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 752dc5ac2577..8cae254d2abc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1503,6 +1503,7 @@ Sync now Auto upload started Auto upload already running + No folder is set up for auto upload This will remove the folder and auto-upload configuration. Any unfinished uploads will be canceled. This folder is already included in the parent folder’s sync, which may cause duplicate uploads Sync anyway diff --git a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt index d62650c281b5..f8698c15a4d6 100644 --- a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt +++ b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt @@ -10,7 +10,6 @@ package com.nextcloud.client.jobs import android.content.Context import android.net.Uri -import androidx.work.Data import androidx.work.WorkerParameters import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.device.PowerManagementService @@ -72,7 +71,6 @@ class ContentObserverWorkTest { ) val uri: Uri = Mockito.mock(Uri::class.java) whenever(params.triggeredContentUris).thenReturn(listOf(uri)) - whenever(params.inputData).thenReturn(Data.EMPTY) } @After @@ -98,7 +96,7 @@ class ContentObserverWorkTest { } @Test - fun power_saving_stops_a_regular_run_before_folders_are_read() { + fun power_saving_stops_a_run_before_folders_are_read() { runBlocking { whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) @@ -108,23 +106,6 @@ class ContentObserverWorkTest { } } - @Test - fun power_saving_does_not_stop_a_run_the_user_asked_for() { - runBlocking { - whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) - whenever(params.inputData).thenReturn( - Data.Builder() - .putBoolean(ContentObserverWork.OVERRIDE_POWER_SAVING, true) - .build() - ) - whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(0) - - worker.doWork() - - verify(folderProvider).countEnabledSyncedFolders() - } - } - @Test @Ignore("TODO: needs further refactoring") fun sync_is_triggered() { diff --git a/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt b/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt index 0ee98ae602dc..e25ab1b23d0d 100644 --- a/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt +++ b/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt @@ -8,6 +8,7 @@ package com.owncloud.android.utils import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.autoUpload.AutoUploadRequestResult import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.SyncedFolderProvider import org.junit.Assert.assertEquals @@ -17,12 +18,14 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever class FilesSyncHelperTest { private val backgroundJobManager: BackgroundJobManager = mock() - private fun syncedFolder(enabled: Boolean): SyncedFolder = mock { + private fun syncedFolder(id: Long, enabled: Boolean): SyncedFolder = mock { + on { this.id } doReturn id on { isEnabled } doReturn enabled } @@ -32,43 +35,52 @@ class FilesSyncHelperTest { @Test fun `sync now starts only enabled folders and asks them to ignore power saving`() { - val enabled = syncedFolder(enabled = true) - val disabled = syncedFolder(enabled = false) + val enabled = syncedFolder(id = 1, enabled = true) + val disabled = syncedFolder(id = 2, enabled = false) - val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + val result = FilesSyncHelper.startAutoUploadIgnoringPowerSaving( provider(enabled, disabled), - backgroundJobManager, - overridePowerSaving = true + backgroundJobManager ) - assertEquals(1, startedFolderCount) + assertEquals(AutoUploadRequestResult.STARTED, result) verify(backgroundJobManager).startAutoUpload(enabled, true) verify(backgroundJobManager, never()).startAutoUpload(disabled, true) } @Test - fun `scheduled runs keep the power saving check enabled`() { - val enabled = syncedFolder(enabled = true) + fun `sync now leaves a folder alone that already ignores power saving`() { + val running = syncedFolder(id = 1, enabled = true) + whenever(backgroundJobManager.isAutoUploadIgnoringPowerSavingScheduled(running.id)).thenReturn(true) - val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( - provider(enabled), - backgroundJobManager, - overridePowerSaving = false + val result = FilesSyncHelper.startAutoUploadIgnoringPowerSaving(provider(running), backgroundJobManager) + + assertEquals(AutoUploadRequestResult.ALREADY_RUNNING, result) + verify(backgroundJobManager, never()).startAutoUpload(any(), any()) + } + + @Test + fun `sync now reports that there is nothing to upload without an enabled folder`() { + val result = FilesSyncHelper.startAutoUploadIgnoringPowerSaving( + provider(syncedFolder(id = 1, enabled = false)), + backgroundJobManager ) - assertEquals(1, startedFolderCount) - verify(backgroundJobManager).startAutoUpload(enabled, false) + assertEquals(AutoUploadRequestResult.NO_ENABLED_FOLDER, result) + verify(backgroundJobManager, never()).startAutoUpload(any(), any()) } @Test - fun `no enabled folder reports nothing to sync`() { + fun `scheduled runs keep the power saving check enabled`() { + val enabled = syncedFolder(id = 1, enabled = true) + val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( - provider(syncedFolder(enabled = false)), + provider(enabled), backgroundJobManager, - overridePowerSaving = true + overridePowerSaving = false ) - assertEquals(0, startedFolderCount) - verify(backgroundJobManager, never()).startAutoUpload(any(), any()) + assertEquals(1, startedFolderCount) + verify(backgroundJobManager).startAutoUpload(enabled, false) } } From 433c91c18503a2b6afba3eeb714e8657e49861d1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 09:27:16 +0200 Subject: [PATCH 11/12] instead of using random minutes to prevent sync now use worker Signed-off-by: alperozturk96 --- .../nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt index 57e55568f8b7..003c6c98186d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ From 11f4710d39c1024ec53aaf43b6c776c82917fbc7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 09:47:52 +0200 Subject: [PATCH 12/12] instead of using random minutes to prevent sync now use worker Signed-off-by: alperozturk96 --- .../client/jobs/BackgroundJobManager.kt | 7 +++++-- .../client/jobs/BackgroundJobManagerImpl.kt | 11 +++++++--- .../client/jobs/ContentObserverWork.kt | 11 +++++++--- .../jobs/autoUpload/AutoUploadWorker.kt | 2 +- .../ui/component/UploadWarningCard.kt | 4 ++-- .../utils/extensions/WorkManagerExtensions.kt | 4 ++-- .../java/com/owncloud/android/MainApp.java | 2 +- .../files/BootupBroadcastReceiver.java | 2 +- .../client/jobs/ContentObserverWorkTest.kt | 21 ++++++++++++++++++- 9 files changed, 48 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 044f4fbb79c3..c65e1d36650b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -35,10 +35,13 @@ interface BackgroundJobManager { * * This call is idempotent - there will be only one scheduled job * regardless of number of calls. + * + * @param overridePowerSaving lets the triggered sync run even while the device is in power saving mode. + * The worker reschedules itself without the override afterwards, so it applies to a single run. */ - fun scheduleContentObserverJob() + fun scheduleContentObserverJob(overridePowerSaving: Boolean = false) - fun isContentObserverJobScheduled(): Boolean + fun isContentObserverRunning(): Boolean /** * Schedule periodic contacts backups job. Operating system will diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 2a3949f414b0..c471ae4976c2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -37,7 +37,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.jobs.worker.WorkerFilesPayload import com.nextcloud.client.preferences.AppPreferences -import com.nextcloud.utils.extensions.isUniqueWorkScheduled +import com.nextcloud.utils.extensions.isUniqueWorkRunning import com.nextcloud.utils.extensions.isWorkScheduled import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SyncedFolder @@ -467,7 +467,11 @@ internal class BackgroundJobManagerImpl( } @Suppress("MagicNumber") - override fun scheduleContentObserverJob() { + override fun scheduleContentObserverJob(overridePowerSaving: Boolean) { + val arguments = Data.Builder() + .putBoolean(ContentObserverWork.OVERRIDE_POWER_SAVING, overridePowerSaving) + .build() + val constrains = Constraints.Builder() .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) @@ -480,12 +484,13 @@ internal class BackgroundJobManagerImpl( val request = oneTimeRequestBuilder(ContentObserverWork::class, JOB_CONTENT_OBSERVER) .setConstraints(constrains) + .setInputData(arguments) .build() workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.REPLACE, request) } - override fun isContentObserverJobScheduled(): Boolean = workManager.isUniqueWorkScheduled(JOB_CONTENT_OBSERVER) + override fun isContentObserverRunning(): Boolean = workManager.isUniqueWorkRunning(JOB_CONTENT_OBSERVER) private fun autoUploadWorkName(syncedFolderID: Long): String = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt index 214b85239b43..cc55f29fc004 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt @@ -12,6 +12,7 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper +import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.FilesSyncHelper @@ -36,12 +37,16 @@ class ContentObserverWork( companion object { private const val TAG = "🔍" + "ContentObserverWork" + const val OVERRIDE_POWER_SAVING = "overridePowerSaving" } + private val overridePowerSaving: Boolean + get() = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { val workerName = BackgroundJobManagerImpl.formatClassTag(this@ContentObserverWork::class) backgroundJobManager.logStartOfWorker(workerName) - Log_OC.d(TAG, "started") + Log_OC.d(TAG, "started - override power saving: $overridePowerSaving") try { if (params.triggeredContentUris.isNotEmpty()) { @@ -72,7 +77,7 @@ class ContentObserverWork( } private suspend fun checkAndTriggerAutoUpload() = withContext(Dispatchers.IO) { - if (powerManagementService.isPowerSavingEnabled) { + if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) { Log_OC.w(TAG, "⚡ Power saving mode active — skipping file sync.") return@withContext } @@ -116,7 +121,7 @@ class ContentObserverWork( FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( syncedFolderProvider, backgroundJobManager, - false + overridePowerSaving ) Log_OC.d(TAG, "✅ auto upload triggered successfully for ${contentUris.size} file(s).") } catch (e: Exception) { 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 1df94d167434..56f89c47cf94 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 @@ -103,7 +103,7 @@ class AutoUploadWorker( } if (powerManagementService.isPowerSavingEnabled) { - Log_OC.w(TAG, "power saving mode enabled") + Log_OC.w(TAG, "power saving mode enabled - override power saving: $overridePowerSaving") } // insert entries based on selected local storage path diff --git a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index cfc15e568f22..8b9b30fea601 100644 --- a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -120,8 +120,8 @@ class UploadWarningCard( private fun startAutoUploadIgnoringBatterySaver(view: View) { scope.launch { val result = withContext(Dispatchers.IO) { - if (!backgroundJobManager.isContentObserverJobScheduled()) { - backgroundJobManager.scheduleContentObserverJob() + if (!backgroundJobManager.isContentObserverRunning()) { + backgroundJobManager.scheduleContentObserverJob(overridePowerSaving = true) } FilesSyncHelper.startAutoUploadIgnoringPowerSaving(syncedFolderProvider, backgroundJobManager) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt index 1e6df23941a0..3ebf5c10af25 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt @@ -25,8 +25,8 @@ fun WorkManager.isWorkScheduled(tag: String): Boolean = getWorkInfosByTag(tag).h /** * Unique work names are not tags, [getWorkInfosByTag] never matches them. */ -fun WorkManager.isUniqueWorkScheduled(uniqueWorkName: String): Boolean = - getWorkInfosForUniqueWork(uniqueWorkName).hasWorkIn(PENDING_STATES) +fun WorkManager.isUniqueWorkRunning(uniqueWorkName: String): Boolean = + getWorkInfosForUniqueWork(uniqueWorkName).hasWorkIn(RUNNING_STATES) private fun ListenableFuture>.hasWorkIn(stateConditions: List): Boolean { var workInfoList: List = emptyList() diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index f08bde45fee9..d9971e05ae4c 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -335,7 +335,7 @@ public void onCreate() { } Log_OC.d(TAG, "scheduleContentObserverJob, called"); - backgroundJobManager.scheduleContentObserverJob(); + backgroundJobManager.scheduleContentObserverJob(false); initSyncOperations(this, preferences, diff --git a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java index 8eb62f17a9c9..9bb3e0820d07 100644 --- a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java +++ b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java @@ -71,7 +71,7 @@ public void onReceive(Context context, Intent intent) { viewThemeUtils, walledCheckCache); Log_OC.d(TAG, "scheduleContentObserverJob, called"); - backgroundJobManager.scheduleContentObserverJob(); + backgroundJobManager.scheduleContentObserverJob(false); MainApp.initContactsBackup(accountManager, backgroundJobManager); } else { Log_OC.d(TAG, "Getting wrong intent: " + intent.getAction()); diff --git a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt index f8698c15a4d6..d62650c281b5 100644 --- a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt +++ b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt @@ -10,6 +10,7 @@ package com.nextcloud.client.jobs import android.content.Context import android.net.Uri +import androidx.work.Data import androidx.work.WorkerParameters import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.device.PowerManagementService @@ -71,6 +72,7 @@ class ContentObserverWorkTest { ) val uri: Uri = Mockito.mock(Uri::class.java) whenever(params.triggeredContentUris).thenReturn(listOf(uri)) + whenever(params.inputData).thenReturn(Data.EMPTY) } @After @@ -96,7 +98,7 @@ class ContentObserverWorkTest { } @Test - fun power_saving_stops_a_run_before_folders_are_read() { + fun power_saving_stops_a_regular_run_before_folders_are_read() { runBlocking { whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) @@ -106,6 +108,23 @@ class ContentObserverWorkTest { } } + @Test + fun power_saving_does_not_stop_a_run_the_user_asked_for() { + runBlocking { + whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) + whenever(params.inputData).thenReturn( + Data.Builder() + .putBoolean(ContentObserverWork.OVERRIDE_POWER_SAVING, true) + .build() + ) + whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(0) + + worker.doWork() + + verify(folderProvider).countEnabledSyncedFolders() + } + } + @Test @Ignore("TODO: needs further refactoring") fun sync_is_triggered() {