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..3a951e17ab90 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -120,6 +120,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() @@ -140,7 +144,7 @@ interface BackgroundJobManager { fun getFileUploads(user: User): LiveData> fun cancelFilesUploadJob(user: User) fun isStartFileUploadJobScheduled(accountName: String): Boolean - + 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 4a28b1715dea..81709351ab86 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -106,6 +106,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" @@ -482,9 +484,24 @@ internal class BackgroundJobManagerImpl( workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.REPLACE, request) } + 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 + // the sync now button starts this folder and also lets the content observer request it, replacing the + // running one would cancel it mid upload + if (overridePowerSaving && isAutoUploadIgnoringPowerSavingScheduled(syncedFolderID)) { + Log_OC.d(TAG, "auto upload ignoring power saving already running for folder $syncedFolderID") + return + } + val arguments = Data.Builder() .putBoolean(AutoUploadWorker.OVERRIDE_POWER_SAVING, overridePowerSaving) .putLong(AutoUploadWorker.SYNCED_FOLDER_ID, syncedFolderID) @@ -495,9 +512,9 @@ internal class BackgroundJobManagerImpl( .setRequiresCharging(syncedFolder.isChargingOnly) .build() - val request = oneTimeRequestBuilder( + val requestBuilder = oneTimeRequestBuilder( jobClass = AutoUploadWorker::class, - jobName = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID + jobName = autoUploadWorkName(syncedFolderID) ) .setInputData(arguments) .setConstraints(constraints) @@ -506,12 +523,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( - JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID, - ExistingWorkPolicy.KEEP, - request + autoUploadWorkName(syncedFolderID), + policy, + requestBuilder.build() ) } 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..2e78d8a071c7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRequestResult.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import com.owncloud.android.R + +enum class AutoUploadRequestResult { + STARTED, + ALREADY_RUNNING, + NO_ENABLED_FOLDER; + + val messageId: Int + get() = when (this) { + STARTED -> R.string.auto_upload_sync_now_started + ALREADY_RUNNING -> R.string.auto_upload_sync_now_running + NO_ENABLED_FOLDER -> R.string.auto_upload_sync_now_no_folder + } +} 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..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 @@ -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 { @@ -100,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 @@ -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/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index b0af6ec9abd5..d668deb22cea 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,25 @@ 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.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 +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) { @@ -33,12 +45,19 @@ class UploadWarningCard( binding.root.setVisibleIf(isBatterySaver || !isIgnoringOptimization) if (isBatterySaver) { - viewThemeUtils.material.themeCardView(binding.batterySaverLayout) + viewThemeUtils.material.run { + themeCardView(binding.batterySaverLayout) + colorMaterialButtonPrimaryBorderless(binding.batterySaverButton) + colorMaterialButtonPrimaryBorderless(binding.syncNowButton) + } + binding.batterySaverLayout.visibility = View.VISIBLE binding.batterySaverButton.setOnClickListener { openBatterySaverPage() } - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.batterySaverButton) + binding.syncNowButton.setOnClickListener { + startAutoUploadIgnoringBatterySaver(it) + } } else { binding.batterySaverLayout.visibility = View.GONE } @@ -94,4 +113,14 @@ class UploadWarningCard( intent.data = "package:${context.packageName}".toUri() context.startActivity(intent) } + + private fun startAutoUploadIgnoringBatterySaver(view: View) { + scope.launch { + val result = withContext(Dispatchers.IO) { + FilesSyncHelper.startAutoUploadIgnoringPowerSaving(syncedFolderProvider, backgroundJobManager) + } + + DisplayUtils.showSnackMessage(view, result.messageId) + } + } } 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..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 @@ -167,7 +167,14 @@ class SyncedFoldersActivity : super.onCreate(savedInstanceState) binding = SyncedFoldersLayoutBinding.inflate(layoutInflater) setContentView(binding.root) - uploadWarningCard = UploadWarningCard(this, powerManagementService, viewThemeUtils) + uploadWarningCard = UploadWarningCard( + this, + powerManagementService, + syncedFolderProvider, + backgroundJobManager, + lifecycleScope, + viewThemeUtils + ) if (intent != null && intent.extras != null) { val accountName = intent.extras!!.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT) val optionalUser = user @@ -405,7 +412,7 @@ class SyncedFoldersActivity : syncedFolder.remotePath, syncedFolder.isWifiOnly, syncedFolder.isChargingOnly, - syncedFolder.isExisting, + syncedFolder.alsoUploadExistingFiles(), syncedFolder.isSubfolderByDate, syncedFolder.account, syncedFolder.uploadAction, @@ -437,7 +444,7 @@ class SyncedFoldersActivity : syncedFolder.remotePath, syncedFolder.isWifiOnly, syncedFolder.isChargingOnly, - syncedFolder.isExisting, + syncedFolder.alsoUploadExistingFiles(), syncedFolder.isSubfolderByDate, syncedFolder.account, syncedFolder.uploadAction, @@ -852,7 +859,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..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 @@ -93,7 +93,14 @@ class UploadListActivity : binding = UploadListLayoutBinding.inflate(layoutInflater) val binding = binding!! setContentView(binding.getRoot()) - uploadWarningCard = UploadWarningCard(this, powerManagementService, viewThemeUtils) + uploadWarningCard = UploadWarningCard( + this, + powerManagementService, + syncedFolderProvider, + backgroundJobManager, + lifecycleScope, + viewThemeUtils + ) swipeListRefreshLayout = binding.swipeContainingList // this activity has no file really bound, it's for multiple accounts at the same time; should no inherit @@ -179,6 +186,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() 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..7e0e8f9fdbb7 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -12,6 +12,7 @@ package com.owncloud.android.utils 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.owncloud.android.datamodel.SyncedFolderProvider @@ -43,13 +44,35 @@ 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 + } + + fun startAutoUploadIgnoringPowerSaving( + provider: SyncedFolderProvider, + 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 + } + + 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/layout/upload_warning_card.xml b/app/src/main/res/layout/upload_warning_card.xml index 4155c8bcaaaa..1e37f351367b 100644 --- a/app/src/main/res/layout/upload_warning_card.xml +++ b/app/src/main/res/layout/upload_warning_card.xml @@ -58,15 +58,29 @@ 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..8cae254d2abc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1500,6 +1500,10 @@ Nextcloud\'s background activity is limited by battery optimization. Open battery saver settings Allow background activity + 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 53846287b2db..f8698c15a4d6 100644 --- a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt +++ b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt @@ -27,6 +27,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 @@ -94,6 +95,17 @@ class ContentObserverWorkTest { } } + @Test + fun power_saving_stops_a_run_before_folders_are_read() { + runBlocking { + whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) + + worker.doWork() + + verify(folderProvider, never()).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 new file mode 100644 index 000000000000..e25ab1b23d0d --- /dev/null +++ b/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt @@ -0,0 +1,86 @@ +/* + * 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.nextcloud.client.jobs.autoUpload.AutoUploadRequestResult +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 +import org.mockito.kotlin.whenever + +class FilesSyncHelperTest { + + private val backgroundJobManager: BackgroundJobManager = mock() + + private fun syncedFolder(id: Long, enabled: Boolean): SyncedFolder = mock { + on { this.id } doReturn id + 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(id = 1, enabled = true) + val disabled = syncedFolder(id = 2, enabled = false) + + val result = FilesSyncHelper.startAutoUploadIgnoringPowerSaving( + provider(enabled, disabled), + backgroundJobManager + ) + + assertEquals(AutoUploadRequestResult.STARTED, result) + verify(backgroundJobManager).startAutoUpload(enabled, true) + verify(backgroundJobManager, never()).startAutoUpload(disabled, true) + } + + @Test + 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 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(AutoUploadRequestResult.NO_ENABLED_FOLDER, result) + verify(backgroundJobManager, never()).startAutoUpload(any(), any()) + } + + @Test + fun `scheduled runs keep the power saving check enabled`() { + val enabled = syncedFolder(id = 1, enabled = true) + + val startedFolderCount = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + provider(enabled), + backgroundJobManager, + overridePowerSaving = false + ) + + assertEquals(1, startedFolderCount) + verify(backgroundJobManager).startAutoUpload(enabled, false) + } +}