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 @@ -25,9 +25,9 @@ import com.nextcloud.client.integrations.deck.DeckApi
import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker
import com.nextcloud.client.jobs.autoUpload.FileSystemRepository
import com.nextcloud.client.jobs.download.FileDownloadWorker
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
import com.nextcloud.client.jobs.metadata.MetadataWorker
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.logger.Logger
import com.nextcloud.client.network.ConnectivityService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,7 @@ interface BackgroundJobManager {

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

fun schedulePeriodicFilesSyncJob(syncedFolder: SyncedFolder)

fun startAutoUploadImmediately(
fun startAutoUpload(
syncedFolder: SyncedFolder,
overridePowerSaving: Boolean = false,
contentUris: Array<String?> = arrayOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import com.owncloud.android.operations.DownloadType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.Duration
import java.util.Date
import java.util.UUID
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -101,8 +102,6 @@ internal class BackgroundJobManagerImpl(

const val JOB_TEST = "test_job"

const val MAX_CONTENT_TRIGGER_DELAY_MS = 10000L

const val TAG_PREFIX_NAME = "name"
const val TAG_PREFIX_USER = "user"
const val TAG_PREFIX_CLASS = "class"
Expand Down Expand Up @@ -269,13 +268,15 @@ internal class BackgroundJobManagerImpl(
return workInfo.map { it -> it.map { fromWorkInfo(it) ?: JobInfo() }.sortedBy { it.started }.reversed() }
}

@Suppress("MagicNumber")
override fun scheduleContentObserverJob() {
val constrains = Constraints.Builder()
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
.setTriggerContentMaxDelay(MAX_CONTENT_TRIGGER_DELAY_MS, TimeUnit.MILLISECONDS)
.setTriggerContentUpdateDelay(Duration.ofSeconds(5))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating 5, 10 as a const value to fix MagicNumber is not good for this use case. Duration.ofSeconds(5) clearly indicates what it is.

.setTriggerContentMaxDelay(Duration.ofSeconds(10))
.build()

val request = oneTimeRequestBuilder(ContentObserverWork::class, JOB_CONTENT_OBSERVER)
Expand Down Expand Up @@ -477,40 +478,7 @@ internal class BackgroundJobManagerImpl(
)
}

override fun schedulePeriodicFilesSyncJob(syncedFolder: SyncedFolder) {
val syncedFolderID = syncedFolder.id

val arguments = Data.Builder()
.putLong(AutoUploadWorker.SYNCED_FOLDER_ID, syncedFolderID)
.build()

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(syncedFolder.isChargingOnly)
.build()

val request = periodicRequestBuilder(
jobClass = AutoUploadWorker::class,
jobName = JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID,
intervalMins = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
constraints = constraints
)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
DEFAULT_BACKOFF_CRITERIA_DELAY_SEC,
TimeUnit.SECONDS
)
.setInputData(arguments)
.build()

workManager.enqueueUniquePeriodicWork(
JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID,
ExistingPeriodicWorkPolicy.KEEP,
request
)
}

override fun startAutoUploadImmediately(
override fun startAutoUpload(
syncedFolder: SyncedFolder,
overridePowerSaving: Boolean,
contentUris: Array<String?>
Expand All @@ -533,6 +501,7 @@ internal class BackgroundJobManagerImpl(
jobName = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID
)
.setInputData(arguments)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
Expand Down
58 changes: 9 additions & 49 deletions app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,25 @@
*/
package com.nextcloud.client.jobs

import android.app.Notification
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.nextcloud.client.device.PowerManagementService
import com.nextcloud.utils.ForegroundServiceHelper
import com.owncloud.android.R
import com.owncloud.android.datamodel.ForegroundServiceType
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.notifications.NotificationUtils
import com.owncloud.android.utils.FilesSyncHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

/**
* This work is triggered when OS detects change in media folders.
*
* It fires media detection job and sync job and finishes immediately.
* It fires media detection worker and auto upload worker and finishes immediately.
*
* This job must not be started on API < 24.
*/
@Suppress("TooGenericExceptionCaught")
class ContentObserverWork(
private val context: Context,
context: Context,
private val params: WorkerParameters,
private val syncedFolderProvider: SyncedFolderProvider,
private val powerManagementService: PowerManagementService,
Expand All @@ -41,8 +34,6 @@ class ContentObserverWork(

companion object {
private const val TAG = "🔍" + "ContentObserverWork"
private const val CHANNEL_ID = NotificationUtils.NOTIFICATION_CHANNEL_CONTENT_OBSERVER
private const val NOTIFICATION_ID = 774
}

override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
Expand All @@ -53,10 +44,6 @@ class ContentObserverWork(
try {
if (params.triggeredContentUris.isNotEmpty()) {
Log_OC.d(TAG, "📸 content observer detected file changes.")

val notificationTitle = context.getString(R.string.content_observer_work_notification_title)
val notification = createNotification(notificationTitle)
updateForegroundInfo(notification)
checkAndTriggerAutoUpload()

// prevent worker fail because of another worker
Expand All @@ -69,46 +56,19 @@ class ContentObserverWork(
Log_OC.d(TAG, "⚠️ triggeredContentUris is empty — nothing to sync.")
}

rescheduleSelf()

val result = Result.success()
backgroundJobManager.logEndOfWorker(workerName, result)
Log_OC.d(TAG, "finished")
result
} catch (e: Exception) {
Log_OC.e(TAG, "❌ Exception in ContentObserverWork: ${e.message}", e)
Result.retry()
} finally {
Log_OC.d(TAG, "🔄" + "re-scheduling job")
backgroundJobManager.scheduleContentObserverJob()
}
}

private suspend fun updateForegroundInfo(notification: Notification) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContentObserverWork should be background job.

val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
NOTIFICATION_ID,
notification,
ForegroundServiceType.DataSync
)
setForeground(foregroundInfo)
}

private fun createNotification(title: String): Notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.setSmallIcon(R.drawable.ic_find_in_page)
.setOngoing(true)
.setSound(null)
.setVibrate(null)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setSilent(true)
.build()

/**
* Re-schedules this observer to ensure continuous monitoring of media changes.
*/
private fun rescheduleSelf() {
Log_OC.d(TAG, "🔁 Rescheduling ContentObserverWork for continued observation.")
backgroundJobManager.scheduleContentObserverJob()
}

private suspend fun checkAndTriggerAutoUpload() = withContext(Dispatchers.IO) {
if (powerManagementService.isPowerSavingEnabled) {
Log_OC.w(TAG, "⚡ Power saving mode active — skipping file sync.")
Expand All @@ -124,15 +84,15 @@ class ContentObserverWork(
val contentUris = params.triggeredContentUris.map { uri ->
// adds uri strings e.g. content://media/external/images/media/2281
uri.toString()
}.toTypedArray()
}.toTypedArray<String?>()
Log_OC.d(TAG, "📄 Content uris detected")

try {
FilesSyncHelper.startAutoUploadImmediatelyWithContentUris(
FilesSyncHelper.startAutoUploadForEnabledSyncedFolders(
syncedFolderProvider,
backgroundJobManager,
false,
contentUris
contentUris,
false
)
Log_OC.d(TAG, "✅ auto upload triggered successfully for ${contentUris.size} file(s).")
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,6 @@ class AutoUploadHelper {
}

fun insertEntries(folder: SyncedFolder, repository: FileSystemRepository) {
val enabledTimestampMs = folder.enabledTimestampMs
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fun SyncedFolder.shouldSkipFile already checking and isExisting is not representing existence of the sync folder it means "Also upload existing files". Function name will be change in different PR.

if (!folder.isEnabled || (!folder.isExisting && enabledTimestampMs < 0)) {
Log_OC.w(
TAG,
"Skipping insertDBEntries: enabled=${folder.isEnabled}, " +
"exists=${folder.isExisting}, enabledTs=$enabledTimestampMs"
)
return
}

when (folder.type) {
MediaFolderType.IMAGE -> {
repository.insertFromUri(MediaStore.Images.Media.INTERNAL_CONTENT_URI, folder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,6 @@ class AutoUploadWorker(
val currentTime = System.currentTimeMillis()
val passedScanInterval = totalScanInterval <= currentTime

Log_OC.d(TAG, "lastScanTimestampMs: " + syncedFolder.lastScanTimestampMs)
Log_OC.d(TAG, "totalScanInterval: $totalScanInterval")
Log_OC.d(TAG, "currentTime: $currentTime")
Log_OC.d(TAG, "passedScanInterval: $passedScanInterval")

if (!passedScanInterval && contentUris.isNullOrEmpty() && !overridePowerSaving) {
Log_OC.w(
TAG,
Expand All @@ -199,6 +194,8 @@ class AutoUploadWorker(
return true
}

Log_OC.d(TAG, "starting ...")

return false
}

Expand All @@ -208,6 +205,8 @@ class AutoUploadWorker(
*/
@Suppress("MagicNumber", "TooGenericExceptionCaught")
private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array<String>?) = try {
Log_OC.d(TAG, "collecting file changes")

withContext(Dispatchers.IO) {
if (contentUris.isNullOrEmpty()) {
helper.insertEntries(syncedFolder, repository)
Expand Down Expand Up @@ -289,7 +288,7 @@ class AutoUploadWorker(
Log_OC.w(TAG, "no more files to upload at lastId: $lastId")
break
}
Log_OC.d(TAG, "Processing batch: lastId=$lastId, count=${filePathsWithIds.size}")
Log_OC.d(TAG, "started, processing batch: lastId=$lastId, count=${filePathsWithIds.size}")

filePathsWithIds.forEach { (path, id) ->
val file = File(path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,4 +396,7 @@ default void onDarkThemeModeChanged(DarkMode mode) {

String getLastDisplayedAccountName();
void setLastDisplayedAccountName(String lastDisplayedAccountName);

boolean startAutoUploadOnStart();
void setLastAutoUploadOnStartTime(long timeInMillisecond);
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,19 @@ public final class AppPreferencesImpl implements AppPreferences {

private static final String PREF_LAST_DISPLAYED_ACCOUNT_NAME = "last_displayed_user";

private static final String AUTO_PREF__LAST_AUTO_UPLOAD_ON_START = "last_auto_upload_on_start";


private static final String LOG_ENTRY = "log_entry";

private final Context context;
private final SharedPreferences preferences;
private final UserAccountManager userAccountManager;
private final ListenerRegistry listeners;

private static final int AUTO_UPLOAD_ON_START_DEBOUNCE_IN_MINUTES = 10;
private static final long AUTO_UPLOAD_ON_START_DEBOUNCE_MS = AUTO_UPLOAD_ON_START_DEBOUNCE_IN_MINUTES * 60 * 1000L;

/**
* Adapter delegating raw {@link SharedPreferences.OnSharedPreferenceChangeListener} calls with key-value pairs to
* respective {@link com.nextcloud.client.preferences.AppPreferences.Listener} method.
Expand Down Expand Up @@ -849,4 +855,16 @@ public String getLastDisplayedAccountName() {
public void setLastDisplayedAccountName(String lastDisplayedAccountName) {
preferences.edit().putString(PREF_LAST_DISPLAYED_ACCOUNT_NAME, lastDisplayedAccountName).apply();
}

@Override
public boolean startAutoUploadOnStart() {
long lastRunTime = preferences.getLong(AUTO_PREF__LAST_AUTO_UPLOAD_ON_START, 0L);
long now = System.currentTimeMillis();
return lastRunTime == 0L || (now - lastRunTime) >= AUTO_UPLOAD_ON_START_DEBOUNCE_MS;
}

@Override
public void setLastAutoUploadOnStartTime(long timeInMillisecond) {
preferences.edit().putLong(AUTO_PREF__LAST_AUTO_UPLOAD_ON_START, timeInMillisecond).apply();
}
}
Loading
Loading