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

Filter by extension

Filter by extension

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

fun startImmediateFilesExportJob(files: Collection<OCFile>): LiveData<JobInfo?>

/**
* @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()
Expand All @@ -140,7 +144,7 @@ interface BackgroundJobManager {
fun getFileUploads(user: User): LiveData<List<JobInfo>>
fun cancelFilesUploadJob(user: User)
fun isStartFileUploadJobScheduled(accountName: String): Boolean

fun isAutoUploadIgnoringPowerSavingScheduled(syncedFolderID: Long): Boolean
fun cancelFilesDownloadJob(accountName: String, fileId: Long)

@Suppress("LongParameterList")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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()
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
33 changes: 31 additions & 2 deletions app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -294,6 +294,10 @@ public boolean isIgnoringPowerSaveMode() {
return mIgnoringPowerSaveMode;
}

public void setIgnoringPowerSaveMode(boolean ignoringPowerSaveMode) {
mIgnoringPowerSaveMode = ignoringPowerSaveMode;
}

public User getUser() {
return user;
}
Expand Down Expand Up @@ -1002,38 +1006,38 @@ private RemoteOperationResult releaseLocksAndUnlockE2EFolder(FileLock fileLock,
}
// endregion

private RemoteOperationResult checkConditions(File originalFile) {
RemoteOperationResult remoteOperationResult = null;
private RemoteOperationResult<Object> checkConditions(File originalFile) {
RemoteOperationResult<Object> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -405,7 +412,7 @@ class SyncedFoldersActivity :
syncedFolder.remotePath,
syncedFolder.isWifiOnly,
syncedFolder.isChargingOnly,
syncedFolder.isExisting,
syncedFolder.alsoUploadExistingFiles(),
syncedFolder.isSubfolderByDate,
syncedFolder.account,
syncedFolder.uploadAction,
Expand Down Expand Up @@ -437,7 +444,7 @@ class SyncedFoldersActivity :
syncedFolder.remotePath,
syncedFolder.isWifiOnly,
syncedFolder.isChargingOnly,
syncedFolder.isExisting,
syncedFolder.alsoUploadExistingFiles(),
syncedFolder.isSubfolderByDate,
syncedFolder.account,
syncedFolder.uploadAction,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -179,6 +186,8 @@ class UploadListActivity :
accountManager,
powerManagementService
)

loadItems()
}

override fun onStart() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading