Browse Source

Merge pull request #5879 from nextcloud/ezaquarii/migrate-files-sync-job-to-work-manager-api

Migrate files sync related jobs to WorkManager API
Tobias Kaminsky 4 years ago
parent
commit
5db713f02a
26 changed files with 831 additions and 903 deletions
  1. 1 1
      scripts/analysis/findbugs-results.txt
  2. 2 2
      src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt
  3. 1 1
      src/main/AndroidManifest.xml
  4. 7 2
      src/main/java/com/nextcloud/client/etm/EtmViewModel.kt
  5. 4 1
      src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt
  6. 52 5
      src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
  7. 9 0
      src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
  8. 85 8
      src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
  9. 1 0
      src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt
  10. 1 0
      src/main/java/com/nextcloud/client/jobs/ContactsImportWork.kt
  11. 2 22
      src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt
  12. 234 0
      src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt
  13. 244 0
      src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt
  14. 143 0
      src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt
  15. 13 43
      src/main/java/com/owncloud/android/MainApp.java
  16. 4 1
      src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java
  17. 0 273
      src/main/java/com/owncloud/android/jobs/FilesSyncJob.java
  18. 0 278
      src/main/java/com/owncloud/android/jobs/MediaFoldersDetectionJob.java
  19. 0 23
      src/main/java/com/owncloud/android/jobs/NCJobCreator.java
  20. 0 185
      src/main/java/com/owncloud/android/jobs/OfflineSyncJob.java
  21. 9 8
      src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java
  22. 5 14
      src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java
  23. 6 35
      src/main/java/com/owncloud/android/utils/FilesSyncHelper.java
  24. 1 1
      src/main/res/layout/fragment_etm_background_jobs.xml
  25. 6 0
      src/main/res/menu/fragment_etm_background_jobs.xml
  26. 1 0
      src/main/res/values/strings.xml

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-381
+379

+ 2 - 2
src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt

@@ -210,10 +210,10 @@ class BackgroundJobManagerTest {
         }
 
         @Test
-        fun job_is_unique() {
+        fun job_is_unique_and_replaces_previous_job() {
             verify(workManager).enqueueUniqueWork(
                 eq(BackgroundJobManagerImpl.JOB_CONTENT_OBSERVER),
-                eq(ExistingWorkPolicy.KEEP),
+                eq(ExistingWorkPolicy.REPLACE),
                 argThat(IsOneTimeWorkRequest())
             )
         }

+ 1 - 1
src/main/AndroidManifest.xml

@@ -140,7 +140,7 @@
         <activity android:name=".ui.activities.ActivitiesActivity"
             android:configChanges="orientation|screenSize|keyboardHidden" />
         <activity android:name=".ui.activity.SyncedFoldersActivity"/>
-        <receiver android:name="com.owncloud.android.jobs.MediaFoldersDetectionJob$NotificationReceiver" />
+        <receiver android:name="com.nextcloud.client.jobs.MediaFoldersDetectionWork$NotificationReceiver" />
         <receiver android:name="com.owncloud.android.jobs.NotificationJob$NotificationReceiver" />
         <activity android:name=".ui.activity.UploadFilesActivity" />
         <activity android:name=".ui.activity.ExternalSiteWebView"

+ 7 - 2
src/main/java/com/nextcloud/client/etm/EtmViewModel.kt

@@ -39,6 +39,7 @@ import com.owncloud.android.R
 import com.owncloud.android.lib.common.accounts.AccountUtils
 import javax.inject.Inject
 
+@Suppress("LongParameterList") // Dependencies Injection
 class EtmViewModel @Inject constructor(
     private val defaultPreferences: SharedPreferences,
     private val platformAccountManager: AccountManager,
@@ -158,8 +159,12 @@ class EtmViewModel @Inject constructor(
         backgroundJobManager.cancelAllJobs()
     }
 
-    fun startTestJob() {
-        backgroundJobManager.scheduleTestJob()
+    fun startTestJob(periodic: Boolean) {
+        if (periodic) {
+            backgroundJobManager.scheduleTestJob()
+        } else {
+            backgroundJobManager.startImmediateTestJob()
+        }
     }
 
     fun cancelTestJob() {

+ 4 - 1
src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt

@@ -130,7 +130,10 @@ class EtmBackgroundJobsFragment : EtmBaseFragment() {
                 vm.pruneJobs(); true
             }
             R.id.etm_background_jobs_start_test -> {
-                vm.startTestJob(); true
+                vm.startTestJob(periodic = false); true
+            }
+            R.id.etm_background_jobs_schedule_test -> {
+                vm.startTestJob(periodic = true); true
             }
             R.id.etm_background_jobs_cancel_test -> {
                 vm.cancelTestJob(); true

+ 52 - 5
src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -32,26 +32,31 @@ import com.nextcloud.client.core.Clock
 import com.nextcloud.client.device.DeviceInfo
 import com.nextcloud.client.device.PowerManagementService
 import com.nextcloud.client.logger.Logger
+import com.nextcloud.client.network.ConnectivityService
 import com.nextcloud.client.preferences.AppPreferences
 import com.owncloud.android.datamodel.ArbitraryDataProvider
 import com.owncloud.android.datamodel.SyncedFolderProvider
+import com.owncloud.android.datamodel.UploadsStorageManager
 import javax.inject.Inject
 import javax.inject.Provider
 
 /**
- * This factory is responsible for creating all background jobs and for injecting job dependencies.
+ * This factory is responsible for creating all background jobs and for injecting worker dependencies.
  */
+@Suppress("LongParameterList") // satisfied by DI
 class BackgroundJobFactory @Inject constructor(
     private val logger: Logger,
     private val preferences: AppPreferences,
     private val contentResolver: ContentResolver,
     private val clock: Clock,
-    private val powerManagerService: PowerManagementService,
+    private val powerManagementService: PowerManagementService,
     private val backgroundJobManager: Provider<BackgroundJobManager>,
     private val deviceInfo: DeviceInfo,
     private val accountManager: UserAccountManager,
     private val resources: Resources,
-    private val dataProvider: ArbitraryDataProvider
+    private val dataProvider: ArbitraryDataProvider,
+    private val uploadsStorageManager: UploadsStorageManager,
+    private val connectivityService: ConnectivityService
 ) : WorkerFactory() {
 
     override fun createWorker(
@@ -70,7 +75,10 @@ class BackgroundJobFactory @Inject constructor(
             ContentObserverWork::class -> createContentObserverJob(context, workerParameters, clock)
             ContactsBackupWork::class -> createContactsBackupWork(context, workerParameters)
             ContactsImportWork::class -> createContactsImportWork(context, workerParameters)
-            else -> null // falls back to default factory
+            FilesSyncWork::class -> createFilesSyncWork(context, workerParameters)
+            OfflineSyncWork::class -> createOfflineSyncWork(context, workerParameters)
+            MediaFoldersDetectionWork::class -> createMediaFoldersDetectionWork(context, workerParameters)
+            else -> null // caller falls back to default factory
         }
     }
 
@@ -86,7 +94,7 @@ class BackgroundJobFactory @Inject constructor(
                 context,
                 workerParameters,
                 folderResolver,
-                powerManagerService,
+                powerManagementService,
                 backgroundJobManager.get()
             )
         } else {
@@ -113,4 +121,43 @@ class BackgroundJobFactory @Inject constructor(
             contentResolver
         )
     }
+
+    private fun createFilesSyncWork(context: Context, params: WorkerParameters): FilesSyncWork {
+        return FilesSyncWork(
+            context = context,
+            params = params,
+            resources = resources,
+            contentResolver = contentResolver,
+            userAccountManager = accountManager,
+            preferences = preferences,
+            uploadsStorageManager = uploadsStorageManager,
+            connectivityService = connectivityService,
+            powerManagementService = powerManagementService,
+            clock = clock,
+            backgroundJobManager = backgroundJobManager.get()
+        )
+    }
+
+    private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork {
+        return OfflineSyncWork(
+            context = context,
+            params = params,
+            contentResolver = contentResolver,
+            userAccountManager = accountManager,
+            connectivityService = connectivityService,
+            powerManagementService = powerManagementService
+        )
+    }
+
+    private fun createMediaFoldersDetectionWork(context: Context, params: WorkerParameters): MediaFoldersDetectionWork {
+        return MediaFoldersDetectionWork(
+            context,
+            params,
+            resources,
+            contentResolver,
+            accountManager,
+            preferences,
+            clock
+        )
+    }
 }

+ 9 - 0
src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt

@@ -28,6 +28,7 @@ import com.nextcloud.client.account.User
  * This interface allows to control, schedule and monitor all application
  * long-running background tasks, such as periodic checks or synchronization.
  */
+@Suppress("TooManyFunctions") // we expect this implementation to have rich API
 interface BackgroundJobManager {
 
     /**
@@ -89,7 +90,15 @@ interface BackgroundJobManager {
         selectedContacts: IntArray
     ): LiveData<JobInfo?>
 
+    fun schedulePeriodicFilesSyncJob()
+    fun startImmediateFilesSyncJob(skipCustomFolders: Boolean = false, overridePowerSaving: Boolean = false)
+    fun scheduleOfflineSync()
+
+    fun scheduleMediaFoldersDetectionJob()
+    fun startMediaFoldersDetectionJob()
+
     fun scheduleTestJob()
+    fun startImmediateTestJob()
     fun cancelTestJob()
 
     fun pruneJobs()

+ 85 - 8
src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt

@@ -29,6 +29,7 @@ import androidx.work.Data
 import androidx.work.ExistingPeriodicWorkPolicy
 import androidx.work.ExistingWorkPolicy
 import androidx.work.ListenableWorker
+import androidx.work.NetworkType
 import androidx.work.OneTimeWorkRequest
 import androidx.work.Operation
 import androidx.work.PeriodicWorkRequest
@@ -67,6 +68,11 @@ internal class BackgroundJobManagerImpl(
         const val JOB_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup"
         const val JOB_IMMEDIATE_CONTACTS_BACKUP = "immediate_contacts_backup"
         const val JOB_IMMEDIATE_CONTACTS_IMPORT = "immediate_contacts_import"
+        const val JOB_PERIODIC_FILES_SYNC = "periodic_files_sync"
+        const val JOB_IMMEDIATE_FILES_SYNC = "immediate_files_sync"
+        const val JOB_PERIODIC_OFFLINE_SYNC = "periodic_offline_sync"
+        const val JOB_PERIODIC_MEDIA_FOLDER_DETECTION = "periodic_media_folder_detection"
+        const val JOB_IMMEDIATE_MEDIA_FOLDER_DETECTION = "immediate_media_folder_detection"
         const val JOB_TEST = "test_job"
 
         const val MAX_CONTENT_TRIGGER_DELAY_MS = 1500L
@@ -81,6 +87,7 @@ internal class BackgroundJobManagerImpl(
         const val INTERVAL_MINUTE = 60L * INTERVAL_SECOND
         const val INTERVAL_HOUR = 60 * INTERVAL_MINUTE
         const val INTERVAL_24H = 24L * INTERVAL_HOUR
+        const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L
 
         fun formatNameTag(name: String, user: User? = null): String {
             return if (user == null) {
@@ -157,9 +164,16 @@ internal class BackgroundJobManagerImpl(
         jobClass: KClass<out ListenableWorker>,
         jobName: String,
         intervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
+        flexIntervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
         user: User? = null
     ): PeriodicWorkRequest.Builder {
-        val builder = PeriodicWorkRequest.Builder(jobClass.java, intervalMins, TimeUnit.MINUTES)
+        val builder = PeriodicWorkRequest.Builder(
+            jobClass.java,
+            intervalMins,
+            TimeUnit.MINUTES,
+            flexIntervalMins,
+            TimeUnit.MINUTES
+        )
             .addTag(TAG_ALL)
             .addTag(formatNameTag(jobName, user))
             .addTag(formatTimeTag(clock.currentTime))
@@ -203,7 +217,7 @@ internal class BackgroundJobManagerImpl(
             .setConstraints(constrains)
             .build()
 
-        workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.KEEP, request)
+        workManager.enqueueUniqueWork(JOB_CONTENT_OBSERVER, ExistingWorkPolicy.REPLACE, request)
     }
 
     override fun schedulePeriodicContactsBackup(user: User) {
@@ -211,12 +225,11 @@ internal class BackgroundJobManagerImpl(
             .putString(ContactsBackupWork.ACCOUNT, user.accountName)
             .putBoolean(ContactsBackupWork.FORCE, true)
             .build()
-
         val request = periodicRequestBuilder(
-            ContactsBackupWork::class,
-            JOB_PERIODIC_CONTACTS_BACKUP,
-            INTERVAL_24H,
-            user
+            jobClass = ContactsBackupWork::class,
+            jobName = JOB_PERIODIC_CONTACTS_BACKUP,
+            intervalMins = INTERVAL_24H,
+            user = user
         ).setInputData(data).build()
 
         workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_CONTACTS_BACKUP, ExistingPeriodicWorkPolicy.KEEP, request)
@@ -268,10 +281,74 @@ internal class BackgroundJobManagerImpl(
         return workManager.getJobInfo(request.id)
     }
 
+    override fun schedulePeriodicFilesSyncJob() {
+        val request = periodicRequestBuilder(
+            jobClass = FilesSyncWork::class,
+            jobName = JOB_PERIODIC_FILES_SYNC,
+            intervalMins = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES).build()
+        workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, ExistingPeriodicWorkPolicy.REPLACE, request)
+    }
+
+    override fun startImmediateFilesSyncJob(skipCustomFolders: Boolean, overridePowerSaving: Boolean) {
+        val arguments = Data.Builder()
+            .putBoolean(FilesSyncWork.SKIP_CUSTOM, skipCustomFolders)
+            .putBoolean(FilesSyncWork.OVERRIDE_POWER_SAVING, overridePowerSaving)
+            .build()
+
+        val request = oneTimeRequestBuilder(
+            jobClass = FilesSyncWork::class,
+            jobName = JOB_IMMEDIATE_FILES_SYNC)
+            .setInputData(arguments)
+            .build()
+
+        workManager.enqueueUniqueWork(JOB_IMMEDIATE_FILES_SYNC, ExistingWorkPolicy.KEEP, request)
+    }
+
+    override fun scheduleOfflineSync() {
+        val constrains = Constraints.Builder()
+            .setRequiredNetworkType(NetworkType.UNMETERED)
+            .build()
+
+        val request = periodicRequestBuilder(OfflineSyncWork::class, JOB_PERIODIC_OFFLINE_SYNC)
+            .setConstraints(constrains)
+            .build()
+
+        workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_OFFLINE_SYNC, ExistingPeriodicWorkPolicy.KEEP, request)
+    }
+
+    override fun scheduleMediaFoldersDetectionJob() {
+        val request = periodicRequestBuilder(MediaFoldersDetectionWork::class, JOB_PERIODIC_MEDIA_FOLDER_DETECTION)
+            .build()
+
+        workManager.enqueueUniquePeriodicWork(
+            JOB_PERIODIC_MEDIA_FOLDER_DETECTION,
+            ExistingPeriodicWorkPolicy.KEEP,
+            request
+        )
+    }
+
+    override fun startMediaFoldersDetectionJob() {
+        val request = oneTimeRequestBuilder(MediaFoldersDetectionWork::class, JOB_IMMEDIATE_MEDIA_FOLDER_DETECTION)
+            .build()
+
+        workManager.enqueueUniqueWork(
+            JOB_IMMEDIATE_MEDIA_FOLDER_DETECTION,
+            ExistingWorkPolicy.KEEP,
+            request
+        )
+    }
+
     override fun scheduleTestJob() {
         val request = periodicRequestBuilder(TestJob::class, JOB_TEST)
+            .setInitialDelay(DEFAULT_IMMEDIATE_JOB_DELAY_SEC, TimeUnit.SECONDS)
+            .build()
+        workManager.enqueueUniquePeriodicWork(JOB_TEST, ExistingPeriodicWorkPolicy.REPLACE, request)
+    }
+
+    override fun startImmediateTestJob() {
+        val request = oneTimeRequestBuilder(TestJob::class, JOB_TEST)
             .build()
-        workManager.enqueueUniquePeriodicWork(JOB_TEST, ExistingPeriodicWorkPolicy.KEEP, request)
+        workManager.enqueueUniqueWork(JOB_TEST, ExistingWorkPolicy.REPLACE, request)
     }
 
     override fun cancelTestJob() {

+ 1 - 0
src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt

@@ -55,6 +55,7 @@ import java.io.InputStreamReader
 import java.util.ArrayList
 import java.util.Calendar
 
+@Suppress("LongParameterList") // legacy code
 class ContactsBackupWork(
     appContext: Context,
     params: WorkerParameters,

+ 1 - 0
src/main/java/com/nextcloud/client/jobs/ContactsImportWork.kt

@@ -65,6 +65,7 @@ class ContactsImportWork(
         val vCards = ArrayList<VCard>()
 
         var cursor: Cursor? = null
+        @Suppress("TooGenericExceptionCaught") // legacy code
         try {
             val operations = ContactOperations(applicationContext, contactsAccountName, contactsAccountType)
             vCards.addAll(Ezvcard.parse(file).all())

+ 2 - 22
src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt

@@ -24,12 +24,8 @@ import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.work.Worker
 import androidx.work.WorkerParameters
-import com.evernote.android.job.JobRequest
-import com.evernote.android.job.util.support.PersistableBundleCompat
 import com.nextcloud.client.device.PowerManagementService
 import com.owncloud.android.datamodel.SyncedFolderProvider
-import com.owncloud.android.jobs.FilesSyncJob
-import com.owncloud.android.jobs.MediaFoldersDetectionJob
 
 /**
  * This work is triggered when OS detects change in media folders.
@@ -50,7 +46,7 @@ class ContentObserverWork(
     override fun doWork(): Result {
         if (params.triggeredContentUris.size > 0) {
             checkAndStartFileSyncJob()
-            startMediaFolderDetectionJob()
+            backgroundJobManager.startMediaFoldersDetectionJob()
         }
         recheduleSelf()
         return Result.success()
@@ -63,23 +59,7 @@ class ContentObserverWork(
     private fun checkAndStartFileSyncJob() {
         val syncFolders = syncerFolderProvider.countEnabledSyncedFolders() > 0
         if (!powerManagementService.isPowerSavingEnabled && syncFolders) {
-            val persistableBundleCompat = PersistableBundleCompat()
-            persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true)
-
-            JobRequest.Builder(FilesSyncJob.TAG)
-                .startNow()
-                .setExtras(persistableBundleCompat)
-                .setUpdateCurrent(false)
-                .build()
-                .schedule()
+            backgroundJobManager.startImmediateFilesSyncJob(true, false)
         }
     }
-
-    private fun startMediaFolderDetectionJob() {
-        JobRequest.Builder(MediaFoldersDetectionJob.TAG)
-            .startNow()
-            .setUpdateCurrent(false)
-            .build()
-            .schedule()
-    }
 }

+ 234 - 0
src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt

@@ -0,0 +1,234 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * @author Chris Narkiewicz
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 Nextcloud
+ * Copyright (C) 2020 Chris Narkiewicz
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.jobs
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.res.Resources
+import android.os.Build
+import android.os.PowerManager
+import android.os.PowerManager.WakeLock
+import android.text.TextUtils
+import androidx.exifinterface.media.ExifInterface
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.core.Clock
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.MainApp
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.FilesystemDataProvider
+import com.owncloud.android.datamodel.MediaFolderType
+import com.owncloud.android.datamodel.SyncedFolder
+import com.owncloud.android.datamodel.SyncedFolderProvider
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.files.services.FileUploader
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.UploadFileOperation
+import com.owncloud.android.ui.activity.SettingsActivity
+import com.owncloud.android.utils.FileStorageUtils
+import com.owncloud.android.utils.FilesSyncHelper
+import com.owncloud.android.utils.MimeType
+import com.owncloud.android.utils.MimeTypeUtil
+import java.io.File
+import java.text.ParsePosition
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+@Suppress("LongParameterList") // legacy code
+class FilesSyncWork(
+    private val context: Context,
+    params: WorkerParameters,
+    private val resources: Resources,
+    private val contentResolver: ContentResolver,
+    private val userAccountManager: UserAccountManager,
+    private val preferences: AppPreferences,
+    private val uploadsStorageManager: UploadsStorageManager,
+    private val connectivityService: ConnectivityService,
+    private val powerManagementService: PowerManagementService,
+    private val clock: Clock,
+    private val backgroundJobManager: BackgroundJobManager
+) : Worker(context, params) {
+
+    companion object {
+        const val TAG = "FilesSyncJob"
+        const val SKIP_CUSTOM = "skipCustom"
+        const val OVERRIDE_POWER_SAVING = "overridePowerSaving"
+        private const val WAKELOCK_TAG_SEPARATION = ":"
+        private const val WAKELOCK_ACQUIRE_TIMEOUT_MS = 10L * 60L * 1000L
+    }
+
+    override fun doWork(): Result {
+        var wakeLock: WakeLock? = null
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+            wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, MainApp.getAuthority() +
+                WAKELOCK_TAG_SEPARATION + TAG)
+            wakeLock.acquire(WAKELOCK_ACQUIRE_TIMEOUT_MS)
+        }
+        val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false)
+        // If we are in power save mode, better to postpone upload
+        if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) {
+            wakeLock?.release()
+            return Result.success()
+        }
+        val resources = context.resources
+        val lightVersion = resources.getBoolean(R.bool.syncedFolder_light)
+        val skipCustom = inputData.getBoolean(SKIP_CUSTOM, false)
+        FilesSyncHelper.restartJobsIfNeeded(uploadsStorageManager,
+            userAccountManager,
+            connectivityService,
+            powerManagementService)
+        FilesSyncHelper.insertAllDBEntries(preferences, clock, skipCustom)
+        // Create all the providers we'll needq
+        val filesystemDataProvider = FilesystemDataProvider(contentResolver)
+        val syncedFolderProvider = SyncedFolderProvider(contentResolver, preferences, clock)
+        val currentLocale = resources.configuration.locale
+        val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale)
+        dateFormat.timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id)
+        for (syncedFolder in syncedFolderProvider.syncedFolders) {
+            if (syncedFolder.isEnabled && (!skipCustom || MediaFolderType.CUSTOM != syncedFolder.type)) {
+                syncFolder(context, resources, lightVersion, filesystemDataProvider, currentLocale, dateFormat,
+                    syncedFolder)
+            }
+        }
+        wakeLock?.release()
+        return Result.success()
+    }
+
+    @Suppress("LongMethod") // legacy code
+    private fun syncFolder(
+        context: Context,
+        resources: Resources,
+        lightVersion: Boolean,
+        filesystemDataProvider: FilesystemDataProvider,
+        currentLocale: Locale,
+        sFormatter: SimpleDateFormat,
+        syncedFolder: SyncedFolder
+    ) {
+        var remotePath: String?
+        var subfolderByDate: Boolean
+        var uploadAction: Int?
+        var needsCharging: Boolean
+        var needsWifi: Boolean
+        var file: File
+        val accountName = syncedFolder.account
+        val optionalUser = userAccountManager.getUser(accountName)
+        if (!optionalUser.isPresent) {
+            return
+        }
+        val user = optionalUser.get()
+        val arbitraryDataProvider = if (lightVersion) {
+            ArbitraryDataProvider(contentResolver)
+        } else {
+            null
+        }
+        val paths = filesystemDataProvider.getFilesForUpload(
+            syncedFolder.localPath,
+            java.lang.Long.toString(syncedFolder.id)
+        )
+        for (path in paths) {
+            file = File(path)
+            val lastModificationTime = calculateLastModificationTime(file, syncedFolder, sFormatter)
+            val mimeType = MimeTypeUtil.getBestMimeTypeByFilename(file.absolutePath)
+            if (lightVersion) {
+                needsCharging = resources.getBoolean(R.bool.syncedFolder_light_on_charging)
+                needsWifi = arbitraryDataProvider!!.getBooleanValue(accountName,
+                    SettingsActivity.SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI)
+                val uploadActionString = resources.getString(R.string.syncedFolder_light_upload_behaviour)
+                uploadAction = getUploadAction(uploadActionString)
+                subfolderByDate = resources.getBoolean(R.bool.syncedFolder_light_use_subfolders)
+                remotePath = resources.getString(R.string.syncedFolder_remote_folder)
+            } else {
+                needsCharging = syncedFolder.isChargingOnly
+                needsWifi = syncedFolder.isWifiOnly
+                uploadAction = syncedFolder.uploadAction
+                subfolderByDate = syncedFolder.isSubfolderByDate
+                remotePath = syncedFolder.remotePath
+            }
+            FileUploader.uploadNewFile(
+                context,
+                user.toPlatformAccount(),
+                file.absolutePath,
+                FileStorageUtils.getInstantUploadFilePath(
+                    file,
+                    currentLocale,
+                    remotePath,
+                    syncedFolder.localPath,
+                    lastModificationTime,
+                    subfolderByDate
+                ),
+                uploadAction!!,
+                mimeType,
+                true, // create parent folder if not existent
+                UploadFileOperation.CREATED_AS_INSTANT_PICTURE,
+                needsWifi,
+                needsCharging,
+                FileUploader.NameCollisionPolicy.ASK_USER
+            )
+            filesystemDataProvider.updateFilesystemFileAsSentForUpload(path,
+                java.lang.Long.toString(syncedFolder.id))
+        }
+    }
+
+    private fun hasExif(file: File): Boolean {
+        val mimeType = FileStorageUtils.getMimeTypeFromName(file.absolutePath)
+        return MimeType.JPEG.equals(mimeType, ignoreCase = true) || MimeType.TIFF.equals(mimeType, ignoreCase = true)
+    }
+
+    private fun calculateLastModificationTime(
+        file: File,
+        syncedFolder: SyncedFolder,
+        formatter: SimpleDateFormat
+    ): Long {
+        var lastModificationTime = file.lastModified()
+        if (MediaFolderType.IMAGE == syncedFolder.type && hasExif(file)) {
+            @Suppress("TooGenericExceptionCaught") // legacy code
+            try {
+                val exifInterface = ExifInterface(file.absolutePath)
+                val exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME)
+                if (!TextUtils.isEmpty(exifDate)) {
+                    val pos = ParsePosition(0)
+                    val dateTime = formatter.parse(exifDate, pos)
+                    lastModificationTime = dateTime.time
+                }
+            } catch (e: Exception) {
+                Log_OC.d(TAG, "Failed to get the proper time " + e.localizedMessage)
+            }
+        }
+        return lastModificationTime
+    }
+
+    private fun getUploadAction(action: String): Int? {
+        return when (action) {
+            "LOCAL_BEHAVIOUR_FORGET" -> FileUploader.LOCAL_BEHAVIOUR_FORGET
+            "LOCAL_BEHAVIOUR_MOVE" -> FileUploader.LOCAL_BEHAVIOUR_MOVE
+            "LOCAL_BEHAVIOUR_DELETE" -> FileUploader.LOCAL_BEHAVIOUR_DELETE
+            else -> FileUploader.LOCAL_BEHAVIOUR_FORGET
+        }
+    }
+}

+ 244 - 0
src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt

@@ -0,0 +1,244 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * @author Chris Narkiewicz
+ * Copyright (C) 2018 Mario Danic
+ * Copyright (C) 2018 Andy Scherzinger
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.jobs
+
+import android.app.Activity
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.graphics.BitmapFactory
+import android.media.RingtoneManager
+import android.text.TextUtils
+import androidx.core.app.NotificationCompat
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.google.gson.Gson
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.core.Clock
+import com.nextcloud.client.preferences.AppPreferences
+import com.nextcloud.client.preferences.AppPreferencesImpl
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.MediaFoldersModel
+import com.owncloud.android.datamodel.MediaProvider
+import com.owncloud.android.datamodel.SyncedFolderProvider
+import com.owncloud.android.jobs.NotificationJob
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.ui.activity.ManageAccountsActivity
+import com.owncloud.android.ui.activity.SyncedFoldersActivity
+import com.owncloud.android.ui.notifications.NotificationUtils
+import com.owncloud.android.utils.ThemeUtils
+import java.util.ArrayList
+import java.util.Random
+
+@Suppress("LongParameterList") // dependencies injection
+class MediaFoldersDetectionWork constructor(
+    private val context: Context,
+    params: WorkerParameters,
+    private val resources: Resources,
+    private val contentResolver: ContentResolver,
+    private val userAccountManager: UserAccountManager,
+    private val preferences: AppPreferences,
+    private val clock: Clock
+) : Worker(context, params) {
+
+    companion object {
+        const val TAG = "MediaFoldersDetectionJob"
+        const val KEY_MEDIA_FOLDER_PATH = "KEY_MEDIA_FOLDER_PATH"
+        const val KEY_MEDIA_FOLDER_TYPE = "KEY_MEDIA_FOLDER_TYPE"
+        private const val ACCOUNT_NAME_GLOBAL = "global"
+        private const val KEY_MEDIA_FOLDERS = "media_folders"
+        const val NOTIFICATION_ID = "NOTIFICATION_ID"
+        private const val DISABLE_DETECTION_CLICK = "DISABLE_DETECTION_CLICK"
+    }
+
+    private val randomIdGenerator = Random(clock.currentTime)
+
+    @Suppress("LongMethod", "ComplexMethod", "NestedBlockDepth") // legacy code
+    override fun doWork(): Result {
+        val arbitraryDataProvider = ArbitraryDataProvider(contentResolver)
+        val syncedFolderProvider = SyncedFolderProvider(contentResolver, preferences, clock)
+        val gson = Gson()
+        val arbitraryDataString: String
+        val mediaFoldersModel: MediaFoldersModel
+        val imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1, null, true)
+        val videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1, null, true)
+        val imageMediaFolderPaths: MutableList<String> = ArrayList()
+        val videoMediaFolderPaths: MutableList<String> = ArrayList()
+        for (imageMediaFolder in imageMediaFolders) {
+            imageMediaFolderPaths.add(imageMediaFolder.absolutePath)
+        }
+        for (videoMediaFolder in videoMediaFolders) {
+            imageMediaFolderPaths.add(videoMediaFolder.absolutePath)
+        }
+        arbitraryDataString = arbitraryDataProvider.getValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS)
+        if (!TextUtils.isEmpty(arbitraryDataString)) {
+            mediaFoldersModel = gson.fromJson(arbitraryDataString, MediaFoldersModel::class.java)
+            // merge new detected paths with already notified ones
+            for (existingImageFolderPath in mediaFoldersModel.imageMediaFolders) {
+                if (!imageMediaFolderPaths.contains(existingImageFolderPath)) {
+                    imageMediaFolderPaths.add(existingImageFolderPath)
+                }
+            }
+            for (existingVideoFolderPath in mediaFoldersModel.videoMediaFolders) {
+                if (!videoMediaFolderPaths.contains(existingVideoFolderPath)) {
+                    videoMediaFolderPaths.add(existingVideoFolderPath)
+                }
+            }
+            // Store updated values
+            arbitraryDataProvider.storeOrUpdateKeyValue(
+                ACCOUNT_NAME_GLOBAL,
+                KEY_MEDIA_FOLDERS,
+                gson.toJson(MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths))
+            )
+            if (preferences.isShowMediaScanNotifications) {
+                imageMediaFolderPaths.removeAll(mediaFoldersModel.imageMediaFolders)
+                videoMediaFolderPaths.removeAll(mediaFoldersModel.videoMediaFolders)
+                if (!imageMediaFolderPaths.isEmpty() || !videoMediaFolderPaths.isEmpty()) {
+                    val allUsers = userAccountManager.allUsers
+                    val activeUsers: MutableList<User> = ArrayList()
+                    for (account in allUsers) {
+                        if (!arbitraryDataProvider.getBooleanValue(account.toPlatformAccount(),
+                                ManageAccountsActivity.PENDING_FOR_REMOVAL)) {
+                            activeUsers.add(account)
+                        }
+                    }
+                    for (user in activeUsers) {
+                        for (imageMediaFolder in imageMediaFolderPaths) {
+                            val folder = syncedFolderProvider.findByLocalPathAndAccount(imageMediaFolder,
+                                user.toPlatformAccount())
+                            if (folder == null) {
+                                val contentTitle = String.format(
+                                    resources.getString(R.string.new_media_folder_detected),
+                                    resources.getString(R.string.new_media_folder_photos)
+                                )
+                                sendNotification(contentTitle,
+                                    imageMediaFolder.substring(imageMediaFolder.lastIndexOf('/') + 1),
+                                    user,
+                                    imageMediaFolder,
+                                    1)
+                            }
+                        }
+                        for (videoMediaFolder in videoMediaFolderPaths) {
+                            val folder = syncedFolderProvider.findByLocalPathAndAccount(videoMediaFolder,
+                                user.toPlatformAccount())
+                            if (folder == null) {
+                                val contentTitle = String.format(context.getString(R.string.new_media_folder_detected),
+                                    context.getString(R.string.new_media_folder_videos))
+                                sendNotification(contentTitle,
+                                    videoMediaFolder.substring(videoMediaFolder.lastIndexOf('/') + 1),
+                                    user,
+                                    videoMediaFolder,
+                                    2)
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            mediaFoldersModel = MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths)
+            arbitraryDataProvider.storeOrUpdateKeyValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS,
+                gson.toJson(mediaFoldersModel))
+        }
+        return Result.success()
+    }
+
+    private fun sendNotification(contentTitle: String, subtitle: String, user: User, path: String, type: Int) {
+        val notificationId = randomIdGenerator.nextInt()
+        val context = context
+        val intent = Intent(context, SyncedFoldersActivity::class.java)
+        intent.putExtra(NOTIFICATION_ID, notificationId)
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+        intent.putExtra(NotificationJob.KEY_NOTIFICATION_ACCOUNT, user.accountName)
+        intent.putExtra(KEY_MEDIA_FOLDER_PATH, path)
+        intent.putExtra(KEY_MEDIA_FOLDER_TYPE, type)
+        intent.putExtra(SyncedFoldersActivity.EXTRA_SHOW_SIDEBAR, true)
+        val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+        val notificationBuilder = NotificationCompat.Builder(
+            context, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL)
+            .setSmallIcon(R.drawable.notification_icon)
+            .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
+            .setColor(ThemeUtils.primaryColor(context))
+            .setSubText(user.accountName)
+            .setContentTitle(contentTitle)
+            .setContentText(subtitle)
+            .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+            .setAutoCancel(true)
+            .setContentIntent(pendingIntent)
+        val disableDetection = Intent(context, NotificationReceiver::class.java)
+        disableDetection.putExtra(NOTIFICATION_ID, notificationId)
+        disableDetection.action = DISABLE_DETECTION_CLICK
+        val disableIntent = PendingIntent.getBroadcast(
+            context,
+            notificationId,
+            disableDetection,
+            PendingIntent.FLAG_CANCEL_CURRENT
+        )
+        notificationBuilder.addAction(
+            NotificationCompat.Action(
+                R.drawable.ic_close,
+                context.getString(R.string.disable_new_media_folder_detection_notifications),
+                disableIntent
+            )
+        )
+        val configureIntent = PendingIntent.getActivity(
+            context,
+            notificationId,
+            intent,
+            PendingIntent.FLAG_CANCEL_CURRENT
+        )
+        notificationBuilder.addAction(
+            NotificationCompat.Action(
+                R.drawable.ic_settings,
+                context.getString(R.string.configure_new_media_folder_detection_notifications),
+                configureIntent
+            )
+        )
+        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        notificationManager.notify(notificationId, notificationBuilder.build())
+    }
+
+    class NotificationReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            val action = intent.action
+            val notificationId = intent.getIntExtra(NOTIFICATION_ID, 0)
+            val preferences = AppPreferencesImpl.fromContext(context)
+            if (DISABLE_DETECTION_CLICK == action) {
+                Log_OC.d(this, "Disable media scan notifications")
+                preferences.isShowMediaScanNotifications = false
+                cancel(context, notificationId)
+            }
+        }
+
+        private fun cancel(context: Context, notificationId: Int) {
+            val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE) as NotificationManager
+            notificationManager.cancel(notificationId)
+        }
+    }
+}

+ 143 - 0
src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt

@@ -0,0 +1,143 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * @author Chris Narkiewicz
+ * Copyright (C) 2018 Mario Danic
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.jobs
+
+import android.content.ContentResolver
+import android.content.Context
+import android.os.Build
+import android.os.PowerManager
+import android.os.PowerManager.WakeLock
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.network.ConnectivityService
+import com.owncloud.android.MainApp
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation
+import com.owncloud.android.operations.SynchronizeFileOperation
+import com.owncloud.android.utils.FileStorageUtils
+import java.io.File
+
+@Suppress("LongParameterList") // Legacy code
+class OfflineSyncWork constructor(
+    private val context: Context,
+    params: WorkerParameters,
+    private val contentResolver: ContentResolver,
+    private val userAccountManager: UserAccountManager,
+    private val connectivityService: ConnectivityService,
+    private val powerManagementService: PowerManagementService
+) : Worker(context, params) {
+
+    companion object {
+        const val TAG = "OfflineSyncJob"
+        private const val WAKELOCK_TAG_SEPARATION = ":"
+        private const val WAKELOCK_ACQUISITION_TIMEOUT_MS = 10L * 60L * 1000L
+    }
+
+    override fun doWork(): Result {
+        var wakeLock: WakeLock? = null
+        if (!powerManagementService.isPowerSavingEnabled && !connectivityService.isInternetWalled) {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+                val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+                val wakeLockTag = MainApp.getAuthority() + WAKELOCK_TAG_SEPARATION + TAG
+                wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, wakeLockTag)
+                wakeLock.acquire(WAKELOCK_ACQUISITION_TIMEOUT_MS)
+            }
+            val users = userAccountManager.allUsers
+            for (user in users) {
+                val storageManager = FileDataStorageManager(user.toPlatformAccount(), contentResolver)
+                val ocRoot = storageManager.getFileByPath(OCFile.ROOT_PATH)
+                if (ocRoot.storagePath == null) {
+                    break
+                }
+                recursive(File(ocRoot.storagePath), storageManager, user)
+            }
+            wakeLock?.release()
+        }
+        return Result.success()
+    }
+
+    @Suppress("ReturnCount", "ComplexMethod") // legacy code
+    private fun recursive(folder: File, storageManager: FileDataStorageManager, user: User) {
+        val downloadFolder = FileStorageUtils.getSavePath(user.accountName)
+        val folderName = folder.absolutePath.replaceFirst(downloadFolder.toRegex(), "") + OCFile.PATH_SEPARATOR
+        Log_OC.d(TAG, "$folderName: enter")
+        // exit
+        if (folder.listFiles() == null) {
+            return
+        }
+        val ocFolder = storageManager.getFileByPath(folderName)
+        Log_OC.d(TAG, folderName + ": currentEtag: " + ocFolder.etag)
+        // check for etag change, if false, skip
+        val checkEtagOperation = CheckEtagRemoteOperation(ocFolder.remotePath,
+            ocFolder.etagOnServer)
+        val result = checkEtagOperation.execute(user.toPlatformAccount(), context)
+        when (result.code) {
+            ResultCode.ETAG_UNCHANGED -> {
+                Log_OC.d(TAG, "$folderName: eTag unchanged")
+                return
+            }
+            ResultCode.FILE_NOT_FOUND -> {
+                val removalResult = storageManager.removeFolder(ocFolder, true, true)
+                if (!removalResult) {
+                    Log_OC.e(TAG, "removal of " + ocFolder.storagePath + " failed: file not found")
+                }
+                return
+            }
+            ResultCode.ETAG_CHANGED -> Log_OC.d(TAG, "$folderName: eTag changed")
+            else -> Log_OC.d(TAG, "$folderName: eTag changed")
+        }
+        // iterate over downloaded files
+        val files = folder.listFiles { obj: File -> obj.isFile }
+        if (files != null) {
+            for (file in files) {
+                val ocFile = storageManager.getFileByLocalPath(file.path)
+                val synchronizeFileOperation = SynchronizeFileOperation(ocFile.remotePath,
+                    user,
+                    true,
+                    context)
+                synchronizeFileOperation.execute(storageManager, context)
+            }
+        }
+        // recursive into folder
+        val subfolders = folder.listFiles { obj: File -> obj.isDirectory }
+        if (subfolders != null) {
+            for (subfolder in subfolders) {
+                recursive(subfolder, storageManager, user)
+            }
+        }
+        // update eTag
+        @Suppress("TooGenericExceptionCaught") // legacy code
+        try {
+            val updatedEtag = result.data[0] as String
+            ocFolder.etagOnServer = updatedEtag
+            storageManager.saveFile(ocFolder)
+        } catch (e: Exception) {
+            Log_OC.e(TAG, "Failed to update etag on " + folder.absolutePath, e)
+        }
+    }
+}

+ 13 - 43
src/main/java/com/owncloud/android/MainApp.java

@@ -41,7 +41,6 @@ import android.text.TextUtils;
 import android.view.WindowManager;
 
 import com.evernote.android.job.JobManager;
-import com.evernote.android.job.JobRequest;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.appinfo.AppInfo;
@@ -70,7 +69,6 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.datastorage.DataStorageProvider;
 import com.owncloud.android.datastorage.StoragePoint;
-import com.owncloud.android.jobs.MediaFoldersDetectionJob;
 import com.owncloud.android.jobs.NCJobCreator;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -95,7 +93,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
 import javax.net.ssl.SSLContext;
@@ -264,10 +261,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
             new NCJobCreator(
                 getApplicationContext(),
                 accountManager,
-                preferences,
                 uploadsStorageManager,
-                connectivityService,
-                powerManagementService,
                 clock,
                 eventBus,
                 backgroundJobManager
@@ -302,7 +296,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
                 Log_OC.d("Debug", "Failed to disable uri exposure");
             }
         }
-        initSyncOperations(uploadsStorageManager,
+        initSyncOperations(preferences,
+                           uploadsStorageManager,
                            accountManager,
                            connectivityService,
                            powerManagementService,
@@ -311,18 +306,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         initContactsBackup(accountManager, backgroundJobManager);
         notificationChannels();
 
-
-        new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
-            .setPeriodic(TimeUnit.MINUTES.toMillis(15), TimeUnit.MINUTES.toMillis(5))
-            .setUpdateCurrent(true)
-            .build()
-            .schedule();
-
-        new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
-            .startNow()
-            .setUpdateCurrent(false)
-            .build()
-            .schedule();
+        backgroundJobManager.scheduleMediaFoldersDetectionJob();
+        backgroundJobManager.startMediaFoldersDetectionJob();
 
         registerGlobalPassCodeProtection();
     }
@@ -461,11 +446,12 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
     }
 
     public static void initSyncOperations(
+        final AppPreferences preferences,
         final UploadsStorageManager uploadsStorageManager,
         final UserAccountManager accountManager,
         final ConnectivityService connectivityService,
         final PowerManagementService powerManagementService,
-        final BackgroundJobManager jobManager,
+        final BackgroundJobManager backgroundJobManager,
         final Clock clock
     ) {
         updateToAutoUpload();
@@ -477,20 +463,23 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
                                                    Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                 splitOutAutoUploadEntries(clock);
             } else {
-                AppPreferences preferences = AppPreferencesImpl.fromContext(getAppContext());
                 preferences.setAutoUploadSplitEntriesEnabled(true);
             }
         }
 
-        initiateExistingAutoUploadEntries(clock);
+        if (!preferences.isAutoUploadInitialized()) {
+            backgroundJobManager.startImmediateFilesSyncJob(false, false);
+            preferences.setAutoUploadInit(true);
+        }
 
-        FilesSyncHelper.scheduleFilesSyncIfNeeded(mContext, jobManager);
+        FilesSyncHelper.scheduleFilesSyncIfNeeded(mContext, backgroundJobManager);
         FilesSyncHelper.restartJobsIfNeeded(
             uploadsStorageManager,
             accountManager,
             connectivityService,
             powerManagementService);
-        FilesSyncHelper.scheduleOfflineSyncIfNeeded();
+
+        backgroundJobManager.scheduleOfflineSync();
 
         ReceiversHelper.registerNetworkChangeReceiver(uploadsStorageManager,
                                                       accountManager,
@@ -755,25 +744,6 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         }
     }
 
-    private static void initiateExistingAutoUploadEntries(Clock clock) {
-        new Thread(() -> {
-            AppPreferences preferences = AppPreferencesImpl.fromContext(getAppContext());
-            if (!preferences.isAutoUploadInitialized()) {
-                SyncedFolderProvider syncedFolderProvider =
-                    new SyncedFolderProvider(MainApp.getAppContext().getContentResolver(), preferences, clock);
-
-                for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
-                    if (syncedFolder.isEnabled()) {
-                        FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, true);
-                    }
-                }
-
-                preferences.setAutoUploadInit(true);
-            }
-
-        }).start();
-    }
-
     private static void cleanOldEntries(Clock clock) {
         // previous versions of application created broken entries in the SyncedFolderProvider
         // database, and this cleans all that and leaves 1 (newest) entry per synced folder

+ 4 - 1
src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java

@@ -32,6 +32,7 @@ import com.nextcloud.client.core.Clock;
 import com.nextcloud.client.device.PowerManagementService;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ConnectivityService;
+import com.nextcloud.client.preferences.AppPreferences;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -49,6 +50,7 @@ public class BootupBroadcastReceiver extends BroadcastReceiver {
 
     private static final String TAG = BootupBroadcastReceiver.class.getSimpleName();
 
+    @Inject AppPreferences preferences;
     @Inject UserAccountManager accountManager;
     @Inject UploadsStorageManager uploadsStorageManager;
     @Inject ConnectivityService connectivityService;
@@ -67,7 +69,8 @@ public class BootupBroadcastReceiver extends BroadcastReceiver {
         AndroidInjection.inject(this, context);
 
         if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
-            MainApp.initSyncOperations(uploadsStorageManager,
+            MainApp.initSyncOperations(preferences,
+                                       uploadsStorageManager,
                                        accountManager,
                                        connectivityService,
                                        powerManagementService,

+ 0 - 273
src/main/java/com/owncloud/android/jobs/FilesSyncJob.java

@@ -1,273 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Mario Danic
- * @author Chris Narkiewicz
- * Copyright (C) 2017 Mario Danic
- * Copyright (C) 2017 Nextcloud
- * Copyright (C) 2919 Chris Narkiewicz
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.owncloud.android.jobs;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.Build;
-import android.os.PowerManager;
-import android.text.TextUtils;
-
-import com.evernote.android.job.Job;
-import com.evernote.android.job.util.support.PersistableBundleCompat;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.core.Clock;
-import com.nextcloud.client.device.PowerManagementService;
-import com.nextcloud.client.network.ConnectivityService;
-import com.nextcloud.client.preferences.AppPreferences;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.MainApp;
-import com.owncloud.android.R;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.FilesystemDataProvider;
-import com.owncloud.android.datamodel.MediaFolderType;
-import com.owncloud.android.datamodel.SyncedFolder;
-import com.owncloud.android.datamodel.SyncedFolderProvider;
-import com.owncloud.android.datamodel.UploadsStorageManager;
-import com.owncloud.android.files.services.FileUploader;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.operations.UploadFileOperation;
-import com.owncloud.android.ui.activity.SettingsActivity;
-import com.owncloud.android.utils.FileStorageUtils;
-import com.owncloud.android.utils.FilesSyncHelper;
-import com.owncloud.android.utils.MimeType;
-import com.owncloud.android.utils.MimeTypeUtil;
-
-import java.io.File;
-import java.text.ParsePosition;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-import androidx.annotation.NonNull;
-import androidx.exifinterface.media.ExifInterface;
-
-/*
-    Job that:
-        - restarts existing jobs if required
-        - finds new and modified files since we last run this
-        - creates upload tasks
- */
-public class FilesSyncJob extends Job {
-    public static final String TAG = "FilesSyncJob";
-    public static final String SKIP_CUSTOM = "skipCustom";
-    public static final String OVERRIDE_POWER_SAVING = "overridePowerSaving";
-    private static final String WAKELOCK_TAG_SEPARATION = ":";
-
-    private final UserAccountManager userAccountManager;
-    private final AppPreferences preferences;
-    private final UploadsStorageManager uploadsStorageManager;
-    private final ConnectivityService connectivityService;
-    private final PowerManagementService powerManagementService;
-    private final Clock clock;
-
-    FilesSyncJob(final UserAccountManager userAccountManager,
-                        final AppPreferences preferences,
-                        final UploadsStorageManager uploadsStorageManager,
-                        final ConnectivityService connectivityService,
-                        final PowerManagementService powerManagementService,
-                        final Clock clock) {
-        this.userAccountManager = userAccountManager;
-        this.preferences = preferences;
-        this.uploadsStorageManager = uploadsStorageManager;
-        this.connectivityService = connectivityService;
-        this.powerManagementService = powerManagementService;
-        this.clock = clock;
-    }
-
-    @NonNull
-    @Override
-    protected Result onRunJob(@NonNull Params params) {
-        final Context context = MainApp.getAppContext();
-        PowerManager.WakeLock wakeLock = null;
-
-        if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
-            PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-            wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, MainApp.getAuthority() +
-                    WAKELOCK_TAG_SEPARATION + TAG);
-            wakeLock.acquire(10 * 60 * 1000);
-        }
-
-        PersistableBundleCompat bundle = params.getExtras();
-        final boolean overridePowerSaving = bundle.getBoolean(OVERRIDE_POWER_SAVING, false);
-
-        // If we are in power save mode, better to postpone upload
-        if (powerManagementService.isPowerSavingEnabled() && !overridePowerSaving) {
-            if (wakeLock != null) {
-                wakeLock.release();
-            }
-            return Result.SUCCESS;
-        }
-
-        Resources resources = MainApp.getAppContext().getResources();
-        boolean lightVersion = resources.getBoolean(R.bool.syncedFolder_light);
-
-        final boolean skipCustom = bundle.getBoolean(SKIP_CUSTOM, false);
-        FilesSyncHelper.restartJobsIfNeeded(uploadsStorageManager,
-                                            userAccountManager,
-                                            connectivityService,
-                                            powerManagementService);
-        FilesSyncHelper.insertAllDBEntries(preferences, clock, skipCustom, false);
-
-        // Create all the providers we'll need
-        final ContentResolver contentResolver = context.getContentResolver();
-        final FilesystemDataProvider filesystemDataProvider = new FilesystemDataProvider(contentResolver);
-        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver, preferences, clock);
-
-        Locale currentLocale = context.getResources().getConfiguration().locale;
-        SimpleDateFormat sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale);
-        sFormatter.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
-
-        for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
-            if ((syncedFolder.isEnabled()) && (!skipCustom || MediaFolderType.CUSTOM != syncedFolder.getType())) {
-                syncFolder(context, resources, lightVersion, filesystemDataProvider, currentLocale, sFormatter,
-                           syncedFolder);
-            }
-        }
-
-        if (wakeLock != null) {
-            wakeLock.release();
-        }
-
-        return Result.SUCCESS;
-    }
-
-    private void syncFolder(
-        Context context,
-        Resources resources,
-        boolean lightVersion,
-        FilesystemDataProvider filesystemDataProvider,
-        Locale currentLocale,
-        SimpleDateFormat sFormatter,
-        SyncedFolder syncedFolder
-    ) {
-        String accountName = syncedFolder.getAccount();
-        Optional<User> optionalUser = userAccountManager.getUser(accountName);
-        if (!optionalUser.isPresent()) {
-            return;
-        }
-        User user = optionalUser.get();
-
-        ArbitraryDataProvider arbitraryDataProvider = null;
-        if (lightVersion) {
-            arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
-        }
-
-        String remotePath;
-        boolean subfolderByDate;
-        Integer uploadAction;
-        FileUploader.NameCollisionPolicy nameCollisionPolicy;
-        boolean needsCharging;
-        boolean needsWifi;
-        File file;
-        for (String path : filesystemDataProvider.getFilesForUpload(syncedFolder.getLocalPath(),
-                Long.toString(syncedFolder.getId()))) {
-            file = new File(path);
-            Long lastModificationTime = calculateLastModificationTime(file, syncedFolder, sFormatter);
-            String mimeType = MimeTypeUtil.getBestMimeTypeByFilename(file.getAbsolutePath());
-
-            if (lightVersion) {
-                needsCharging = resources.getBoolean(R.bool.syncedFolder_light_on_charging);
-                needsWifi = arbitraryDataProvider.getBooleanValue(accountName,
-                                                                  SettingsActivity.SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI);
-                String uploadActionString = resources.getString(R.string.syncedFolder_light_upload_behaviour);
-                uploadAction = getUploadAction(uploadActionString);
-                nameCollisionPolicy = FileUploader.NameCollisionPolicy.ASK_USER;
-                subfolderByDate = resources.getBoolean(R.bool.syncedFolder_light_use_subfolders);
-                remotePath = resources.getString(R.string.syncedFolder_remote_folder);
-            } else {
-                needsCharging = syncedFolder.isChargingOnly();
-                needsWifi = syncedFolder.isWifiOnly();
-                uploadAction = syncedFolder.getUploadAction();
-                nameCollisionPolicy = FileUploader.NameCollisionPolicy.deserialize(
-                        syncedFolder.getNameCollisionPolicy());
-                subfolderByDate = syncedFolder.isSubfolderByDate();
-                remotePath = syncedFolder.getRemotePath();
-            }
-
-            FileUploader.uploadNewFile(
-                context,
-                user.toPlatformAccount(),
-                file.getAbsolutePath(),
-                FileStorageUtils.getInstantUploadFilePath(
-                        file,
-                        currentLocale,
-                        remotePath,
-                        syncedFolder.getLocalPath(),
-                        lastModificationTime,
-                        subfolderByDate),
-                uploadAction,
-                mimeType,
-                true,           // create parent folder if not existent
-                UploadFileOperation.CREATED_AS_INSTANT_PICTURE,
-                needsWifi,
-                needsCharging,
-                nameCollisionPolicy
-            );
-
-            filesystemDataProvider.updateFilesystemFileAsSentForUpload(path,
-                                                                       Long.toString(syncedFolder.getId()));
-        }
-    }
-
-    private Long calculateLastModificationTime(File file, SyncedFolder syncedFolder, SimpleDateFormat formatter) {
-        Long lastModificationTime = file.lastModified();
-
-        if (MediaFolderType.IMAGE == syncedFolder.getType()) {
-            String mimeTypeString = FileStorageUtils.getMimeTypeFromName(file.getAbsolutePath());
-            if (MimeType.JPEG.equalsIgnoreCase(mimeTypeString)
-                    || MimeType.TIFF.equalsIgnoreCase(mimeTypeString)) {
-                try {
-                    ExifInterface exifInterface = new ExifInterface(file.getAbsolutePath());
-                    String exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME);
-                    if (!TextUtils.isEmpty(exifDate)) {
-                        ParsePosition pos = new ParsePosition(0);
-                        Date dateTime = formatter.parse(exifDate, pos);
-                        lastModificationTime = dateTime.getTime();
-                    }
-                } catch (Exception e) {
-                    Log_OC.d(TAG, "Failed to get the proper time " + e.getLocalizedMessage());
-                }
-            }
-        }
-
-        return lastModificationTime;
-    }
-
-    private Integer getUploadAction(String action) {
-        switch (action) {
-            case "LOCAL_BEHAVIOUR_FORGET":
-                return FileUploader.LOCAL_BEHAVIOUR_FORGET;
-            case "LOCAL_BEHAVIOUR_MOVE":
-                return FileUploader.LOCAL_BEHAVIOUR_MOVE;
-            case "LOCAL_BEHAVIOUR_DELETE":
-                return FileUploader.LOCAL_BEHAVIOUR_DELETE;
-            default:
-                return FileUploader.LOCAL_BEHAVIOUR_FORGET;
-        }
-    }
-}

+ 0 - 278
src/main/java/com/owncloud/android/jobs/MediaFoldersDetectionJob.java

@@ -1,278 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Mario Danic
- * @author Andy Scherzinger
- * @author Chris Narkiewicz
- * Copyright (C) 2018 Mario Danic
- * Copyright (C) 2018 Andy Scherzinger
- * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-package com.owncloud.android.jobs;
-
-import android.app.Activity;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.BitmapFactory;
-import android.media.RingtoneManager;
-import android.text.TextUtils;
-
-import com.evernote.android.job.Job;
-import com.google.gson.Gson;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.core.Clock;
-import com.nextcloud.client.preferences.AppPreferences;
-import com.nextcloud.client.preferences.AppPreferencesImpl;
-import com.owncloud.android.R;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.MediaFolder;
-import com.owncloud.android.datamodel.MediaFoldersModel;
-import com.owncloud.android.datamodel.MediaProvider;
-import com.owncloud.android.datamodel.SyncedFolder;
-import com.owncloud.android.datamodel.SyncedFolderProvider;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.activity.ManageAccountsActivity;
-import com.owncloud.android.ui.activity.SyncedFoldersActivity;
-import com.owncloud.android.ui.notifications.NotificationUtils;
-import com.owncloud.android.utils.ThemeUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Random;
-
-import androidx.annotation.NonNull;
-import androidx.core.app.NotificationCompat;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-@SuppressFBWarnings(value = "PREDICTABLE_RANDOM", justification = "Only used for notification id.")
-public class MediaFoldersDetectionJob extends Job {
-    public static final String TAG = "MediaFoldersDetectionJob";
-
-    public static final String KEY_MEDIA_FOLDER_PATH = "KEY_MEDIA_FOLDER_PATH";
-    public static final String KEY_MEDIA_FOLDER_TYPE = "KEY_MEDIA_FOLDER_TYPE";
-
-    private static final String ACCOUNT_NAME_GLOBAL = "global";
-    private static final String KEY_MEDIA_FOLDERS = "media_folders";
-    public static final String NOTIFICATION_ID = "NOTIFICATION_ID";
-
-    private static final String DISABLE_DETECTION_CLICK = "DISABLE_DETECTION_CLICK";
-
-    private final UserAccountManager userAccountManager;
-    private final Clock clock;
-    private final Random randomId = new Random();
-
-    MediaFoldersDetectionJob(UserAccountManager accountManager, Clock clock) {
-        this.userAccountManager = accountManager;
-        this.clock = clock;
-    }
-
-    @NonNull
-    @Override
-    protected Result onRunJob(@NonNull Params params) {
-        Context context = getContext();
-        ContentResolver contentResolver = context.getContentResolver();
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(contentResolver);
-        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver,
-                                                                             AppPreferencesImpl.fromContext(context),
-                                                                             clock);
-        Gson gson = new Gson();
-        String arbitraryDataString;
-        MediaFoldersModel mediaFoldersModel;
-
-        List<MediaFolder> imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1, null, true);
-        List<MediaFolder> videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1, null, true);
-
-        List<String> imageMediaFolderPaths = new ArrayList<>();
-        List<String> videoMediaFolderPaths = new ArrayList<>();
-
-        for (MediaFolder imageMediaFolder : imageMediaFolders) {
-            imageMediaFolderPaths.add(imageMediaFolder.absolutePath);
-        }
-
-        for (MediaFolder videoMediaFolder : videoMediaFolders) {
-            imageMediaFolderPaths.add(videoMediaFolder.absolutePath);
-        }
-
-        arbitraryDataString = arbitraryDataProvider.getValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS);
-        if (!TextUtils.isEmpty(arbitraryDataString)) {
-            mediaFoldersModel = gson.fromJson(arbitraryDataString, MediaFoldersModel.class);
-
-            // merge new detected paths with already notified ones
-            for (String existingImageFolderPath : mediaFoldersModel.getImageMediaFolders()) {
-                if (!imageMediaFolderPaths.contains(existingImageFolderPath)) {
-                    imageMediaFolderPaths.add(existingImageFolderPath);
-                }
-            }
-
-            for (String existingVideoFolderPath : mediaFoldersModel.getVideoMediaFolders()) {
-                if (!videoMediaFolderPaths.contains(existingVideoFolderPath)) {
-                    videoMediaFolderPaths.add(existingVideoFolderPath);
-                }
-            }
-
-            // Store updated values
-            arbitraryDataProvider.storeOrUpdateKeyValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS, gson.toJson(new
-                MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths)));
-
-            final AppPreferences preferences = AppPreferencesImpl.fromContext(getContext());
-            if (preferences.isShowMediaScanNotifications()) {
-                imageMediaFolderPaths.removeAll(mediaFoldersModel.getImageMediaFolders());
-                videoMediaFolderPaths.removeAll(mediaFoldersModel.getVideoMediaFolders());
-
-                if (!imageMediaFolderPaths.isEmpty() || !videoMediaFolderPaths.isEmpty()) {
-                    List<User> allUsers = userAccountManager.getAllUsers();
-                    List<User> activeUsers = new ArrayList<>();
-                    for (User account : allUsers) {
-                        if (!arbitraryDataProvider.getBooleanValue(account.toPlatformAccount(),
-                                                                   ManageAccountsActivity.PENDING_FOR_REMOVAL)) {
-                            activeUsers.add(account);
-                        }
-                    }
-
-                    for (User user : activeUsers) {
-                        for (String imageMediaFolder : imageMediaFolderPaths) {
-                            final SyncedFolder folder = syncedFolderProvider.findByLocalPathAndAccount(imageMediaFolder,
-                                                                                                       user.toPlatformAccount());
-                            if (folder == null) {
-                                String contentTitle = String.format(context.getString(R.string.new_media_folder_detected),
-                                                                    context.getString(R.string.new_media_folder_photos));
-                                sendNotification(contentTitle,
-                                                imageMediaFolder.substring(imageMediaFolder.lastIndexOf('/') + 1),
-                                                user,
-                                                imageMediaFolder,
-                                                 1);
-                            }
-                        }
-
-                        for (String videoMediaFolder : videoMediaFolderPaths) {
-                            final SyncedFolder folder = syncedFolderProvider.findByLocalPathAndAccount(videoMediaFolder,
-                                                                                                       user.toPlatformAccount());
-                            if (folder == null) {
-                                String contentTitle = String.format(context.getString(R.string.new_media_folder_detected),
-                                                                    context.getString(R.string.new_media_folder_videos));
-                                sendNotification(contentTitle,
-                                                 videoMediaFolder.substring(videoMediaFolder.lastIndexOf('/') + 1),
-                                                 user,
-                                                 videoMediaFolder,
-                                                 2);
-                            }
-                        }
-                    }
-                }
-            }
-
-        } else {
-            mediaFoldersModel = new MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths);
-            arbitraryDataProvider.storeOrUpdateKeyValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS,
-                gson.toJson(mediaFoldersModel));
-        }
-
-        return Result.SUCCESS;
-    }
-
-    private void sendNotification(String contentTitle, String subtitle, User user, String path, int type) {
-        int notificationId = randomId.nextInt();
-
-        Context context = getContext();
-        Intent intent = new Intent(getContext(), SyncedFoldersActivity.class);
-        intent.putExtra(NOTIFICATION_ID, notificationId);
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        intent.putExtra(NotificationJob.KEY_NOTIFICATION_ACCOUNT, user.getAccountName());
-        intent.putExtra(KEY_MEDIA_FOLDER_PATH, path);
-        intent.putExtra(KEY_MEDIA_FOLDER_TYPE, type);
-        intent.putExtra(SyncedFoldersActivity.EXTRA_SHOW_SIDEBAR, true);
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
-
-        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
-            context, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL)
-            .setSmallIcon(R.drawable.notification_icon)
-            .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
-            .setColor(ThemeUtils.primaryColor(getContext()))
-            .setSubText(user.getAccountName())
-            .setContentTitle(contentTitle)
-            .setContentText(subtitle)
-            .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
-            .setAutoCancel(true)
-            .setContentIntent(pendingIntent);
-
-        Intent disableDetection = new Intent(context, NotificationReceiver.class);
-        disableDetection.putExtra(NOTIFICATION_ID, notificationId);
-        disableDetection.setAction(DISABLE_DETECTION_CLICK);
-
-        PendingIntent disableIntent = PendingIntent.getBroadcast(
-            context,
-            notificationId,
-            disableDetection,
-            PendingIntent.FLAG_CANCEL_CURRENT
-        );
-        notificationBuilder.addAction(
-            new NotificationCompat.Action(
-                R.drawable.ic_close,
-                context.getString(R.string.disable_new_media_folder_detection_notifications),
-                disableIntent
-            )
-        );
-
-        PendingIntent configureIntent = PendingIntent.getActivity(
-            context,
-            notificationId,
-            intent,
-            PendingIntent.FLAG_CANCEL_CURRENT
-        );
-        notificationBuilder.addAction(
-            new NotificationCompat.Action(
-                R.drawable.ic_settings,
-                context.getString(R.string.configure_new_media_folder_detection_notifications),
-                configureIntent
-            )
-        );
-
-        NotificationManager notificationManager = (NotificationManager)
-            context.getSystemService(Context.NOTIFICATION_SERVICE);
-
-        if (notificationManager != null) {
-            notificationManager.notify(notificationId, notificationBuilder.build());
-        }
-    }
-
-
-    public static class NotificationReceiver extends BroadcastReceiver {
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            int notificationId = intent.getIntExtra(NOTIFICATION_ID, 0);
-            final AppPreferences preferences = AppPreferencesImpl.fromContext(context);
-
-            if (DISABLE_DETECTION_CLICK.equals(action)) {
-                Log_OC.d(this, "Disable media scan notifications");
-                preferences.setShowMediaScanNotifications(false);
-                cancel(context, notificationId);
-            }
-        }
-
-        private void cancel(Context context, int notificationId) {
-            NotificationManager notificationManager =
-                (NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
-            notificationManager.cancel(notificationId);
-        }
-    }
-}

+ 0 - 23
src/main/java/com/owncloud/android/jobs/NCJobCreator.java

@@ -30,10 +30,7 @@ import com.evernote.android.job.Job;
 import com.evernote.android.job.JobCreator;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.core.Clock;
-import com.nextcloud.client.device.PowerManagementService;
 import com.nextcloud.client.jobs.BackgroundJobManager;
-import com.nextcloud.client.network.ConnectivityService;
-import com.nextcloud.client.preferences.AppPreferences;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 
 import org.greenrobot.eventbus.EventBus;
@@ -48,10 +45,7 @@ public class NCJobCreator implements JobCreator {
 
     private final Context context;
     private final UserAccountManager accountManager;
-    private final AppPreferences preferences;
     private final UploadsStorageManager uploadsStorageManager;
-    private final ConnectivityService connectivityService;
-    private final PowerManagementService powerManagementService;
     private final Clock clock;
     private final EventBus eventBus;
     private final BackgroundJobManager backgroundJobManager;
@@ -59,20 +53,14 @@ public class NCJobCreator implements JobCreator {
     public NCJobCreator(
         Context context,
         UserAccountManager accountManager,
-        AppPreferences preferences,
         UploadsStorageManager uploadsStorageManager,
-        ConnectivityService connectivityServices,
-        PowerManagementService powerManagementService,
         Clock clock,
         EventBus eventBus,
         BackgroundJobManager backgroundJobManager
     ) {
         this.context = context;
         this.accountManager = accountManager;
-        this.preferences = preferences;
         this.uploadsStorageManager = uploadsStorageManager;
-        this.connectivityService = connectivityServices;
-        this.powerManagementService = powerManagementService;
         this.clock = clock;
         this.eventBus = eventBus;
         this.backgroundJobManager = backgroundJobManager;
@@ -87,19 +75,8 @@ public class NCJobCreator implements JobCreator {
                                              backgroundJobManager,
                                              clock,
                                              eventBus);
-            case FilesSyncJob.TAG:
-                return new FilesSyncJob(accountManager,
-                                        preferences,
-                                        uploadsStorageManager,
-                                        connectivityService,
-                                        powerManagementService,
-                                        clock);
-            case OfflineSyncJob.TAG:
-                return new OfflineSyncJob(accountManager, connectivityService, powerManagementService);
             case NotificationJob.TAG:
                 return new NotificationJob(context, accountManager);
-            case MediaFoldersDetectionJob.TAG:
-                return new MediaFoldersDetectionJob(accountManager, clock);
             default:
                 return null;
         }

+ 0 - 185
src/main/java/com/owncloud/android/jobs/OfflineSyncJob.java

@@ -1,185 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Mario Danic
- * @author Chris Narkiewicz
- * Copyright (C) 2018 Mario Danic
- * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-package com.owncloud.android.jobs;
-
-import android.content.Context;
-import android.os.Build;
-import android.os.PowerManager;
-
-import com.evernote.android.job.Job;
-import com.evernote.android.job.JobManager;
-import com.evernote.android.job.JobRequest;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.device.PowerManagementService;
-import com.nextcloud.client.network.ConnectivityService;
-import com.owncloud.android.MainApp;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation;
-import com.owncloud.android.operations.SynchronizeFileOperation;
-import com.owncloud.android.utils.FileStorageUtils;
-
-import java.io.File;
-import java.util.List;
-import java.util.Set;
-
-import androidx.annotation.NonNull;
-
-import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
-import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
-
-public class OfflineSyncJob extends Job {
-    public static final String TAG = "OfflineSyncJob";
-
-    private static final String WAKELOCK_TAG_SEPARATION = ":";
-    private final UserAccountManager userAccountManager;
-    private final ConnectivityService connectivityService;
-    private final PowerManagementService powerManagementService;
-
-    OfflineSyncJob(UserAccountManager userAccountManager, ConnectivityService connectivityService, PowerManagementService powerManagementService) {
-        this.userAccountManager = userAccountManager;
-        this.connectivityService = connectivityService;
-        this.powerManagementService = powerManagementService;
-    }
-
-    @NonNull
-    @Override
-    protected Result onRunJob(@NonNull Params params) {
-        final Context context = getContext();
-
-        PowerManager.WakeLock wakeLock = null;
-        if (!powerManagementService.isPowerSavingEnabled() &&
-                connectivityService.getActiveNetworkType() == JobRequest.NetworkType.UNMETERED &&
-                !connectivityService.isInternetWalled()) {
-            Set<Job> jobs = JobManager.instance().getAllJobsForTag(TAG);
-            for (Job job : jobs) {
-                if (!job.isFinished() && !job.equals(this)) {
-                    return Result.SUCCESS;
-                }
-            }
-
-            if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
-                PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-
-                if (powerManager != null) {
-                    wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, MainApp.getAuthority() +
-                        WAKELOCK_TAG_SEPARATION + TAG);
-                    wakeLock.acquire(10 * 60 * 1000);
-                }
-            }
-
-            List<User> users = userAccountManager.getAllUsers();
-
-            for (User user : users) {
-                FileDataStorageManager storageManager = new FileDataStorageManager(user.toPlatformAccount(),
-                        getContext().getContentResolver());
-
-                OCFile ocRoot = storageManager.getFileByPath(ROOT_PATH);
-
-                if (ocRoot.getStoragePath() == null) {
-                    break;
-                }
-
-                recursive(new File(ocRoot.getStoragePath()), storageManager, user);
-            }
-
-            if (wakeLock != null) {
-                wakeLock.release();
-            }
-        }
-
-        return Result.SUCCESS;
-    }
-
-    private void recursive(File folder, FileDataStorageManager storageManager, User user) {
-        String downloadFolder = FileStorageUtils.getSavePath(user.getAccountName());
-        String folderName = folder.getAbsolutePath().replaceFirst(downloadFolder, "") + PATH_SEPARATOR;
-        Log_OC.d(TAG, folderName + ": enter");
-
-        // exit
-        if (folder.listFiles() == null) {
-            return;
-        }
-
-        OCFile ocFolder = storageManager.getFileByPath(folderName);
-        Log_OC.d(TAG, folderName + ": currentEtag: " + ocFolder.getEtag());
-
-        // check for etag change, if false, skip
-        CheckEtagRemoteOperation checkEtagOperation = new CheckEtagRemoteOperation(ocFolder.getRemotePath(),
-                                                                                   ocFolder.getEtagOnServer());
-        RemoteOperationResult result = checkEtagOperation.execute(user.toPlatformAccount(), getContext());
-
-        // eTag changed, sync file
-        switch (result.getCode()) {
-            case ETAG_UNCHANGED:
-                Log_OC.d(TAG, folderName + ": eTag unchanged");
-                return;
-
-            case FILE_NOT_FOUND:
-                boolean removalResult = storageManager.removeFolder(ocFolder, true, true);
-                if (!removalResult) {
-                    Log_OC.e(TAG, "removal of " + ocFolder.getStoragePath() + " failed: file not found");
-                }
-                return;
-
-            default:
-            case ETAG_CHANGED:
-                Log_OC.d(TAG, folderName + ": eTag changed");
-                break;
-        }
-
-        // iterate over downloaded files
-        File[] files = folder.listFiles(File::isFile);
-
-        if (files != null) {
-            for (File file : files) {
-                OCFile ocFile = storageManager.getFileByLocalPath(file.getPath());
-                SynchronizeFileOperation synchronizeFileOperation = new SynchronizeFileOperation(ocFile.getRemotePath(),
-                                                                                                 user,
-                                                                                                 true,
-                                                                                                 getContext());
-                synchronizeFileOperation.execute(storageManager, getContext());
-            }
-        }
-
-        // recursive into folder
-        File[] subfolders = folder.listFiles(File::isDirectory);
-
-        if (subfolders != null) {
-            for (File subfolder : subfolders) {
-                recursive(subfolder, storageManager, user);
-            }
-        }
-
-        // update eTag
-        try {
-            String updatedEtag = (String) result.getData().get(0);
-            ocFolder.setEtagOnServer(updatedEtag);
-            storageManager.saveFile(ocFolder);
-        } catch (Exception e) {
-            Log_OC.e(TAG, "Failed to update etag on " + folder.getAbsolutePath(), e);
-        }
-    }
-}

+ 9 - 8
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -47,6 +47,8 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.client.core.Clock;
 import com.nextcloud.client.device.PowerManagementService;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.jobs.BackgroundJobManager;
+import com.nextcloud.client.jobs.MediaFoldersDetectionWork;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.java.util.Optional;
 import com.owncloud.android.BuildConfig;
@@ -61,7 +63,6 @@ import com.owncloud.android.datamodel.SyncedFolder;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
 import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.files.services.FileUploader;
-import com.owncloud.android.jobs.MediaFoldersDetectionJob;
 import com.owncloud.android.jobs.NotificationJob;
 import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
 import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
@@ -140,6 +141,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
     @Inject AppPreferences preferences;
     @Inject PowerManagementService powerManagementService;
     @Inject Clock clock;
+    @Inject BackgroundJobManager backgroundJobManager;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -163,11 +165,11 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                 }
             }
 
-            path = getIntent().getStringExtra(MediaFoldersDetectionJob.KEY_MEDIA_FOLDER_PATH);
-            type = getIntent().getIntExtra(MediaFoldersDetectionJob.KEY_MEDIA_FOLDER_TYPE, -1);
+            path = getIntent().getStringExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_PATH);
+            type = getIntent().getIntExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_TYPE, -1);
 
             // Cancel notification
-            int notificationId = getIntent().getIntExtra(MediaFoldersDetectionJob.NOTIFICATION_ID, 0);
+            int notificationId = getIntent().getIntExtra(MediaFoldersDetectionWork.NOTIFICATION_ID, 0);
             NotificationManager notificationManager =
                 (NotificationManager) getSystemService(Activity.NOTIFICATION_SERVICE);
             notificationManager.cancel(notificationId);
@@ -637,8 +639,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         }
 
         if (syncedFolderDisplayItem.isEnabled()) {
-            FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolderDisplayItem, true);
-
+            backgroundJobManager.startImmediateFilesSyncJob(false, false);
             showBatteryOptimizationInfo();
         }
     }
@@ -779,7 +780,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
             // existing synced folder setup to be updated
             syncedFolderProvider.updateSyncFolder(item);
             if (item.isEnabled()) {
-                FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item, true);
+                backgroundJobManager.startImmediateFilesSyncJob(false, false);
             } else {
                 String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
 
@@ -797,7 +798,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         if (storedId != -1) {
             item.setId(storedId);
             if (item.isEnabled()) {
-                FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item, true);
+                backgroundJobManager.startImmediateFilesSyncJob(false, false);
             } else {
                 String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
                 arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);

+ 5 - 14
src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java

@@ -45,6 +45,7 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.core.Clock;
 import com.nextcloud.client.device.PowerManagementService;
+import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.java.util.Optional;
 import com.owncloud.android.R;
@@ -52,7 +53,6 @@ import com.owncloud.android.databinding.UploadListLayoutBinding;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
-import com.owncloud.android.jobs.FilesSyncJob;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -99,6 +99,9 @@ public class UploadListActivity extends FileActivity {
     @Inject
     Clock clock;
 
+    @Inject
+    BackgroundJobManager backgroundJobManager;
+
     private UploadListLayoutBinding binding;
 
     @Override
@@ -182,19 +185,7 @@ public class UploadListActivity extends FileActivity {
     }
 
     private void refresh() {
-        // scan for missing auto uploads files
-        Set<Job> jobs = JobManager.instance().getAllJobsForTag(FilesSyncJob.TAG);
-
-        if (jobs.isEmpty()) {
-            PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();
-            persistableBundleCompat.putBoolean(FilesSyncJob.OVERRIDE_POWER_SAVING, true);
-            new JobRequest.Builder(FilesSyncJob.TAG)
-                .setExact(1_000L)
-                .setUpdateCurrent(false)
-                .setExtras(persistableBundleCompat)
-                .build()
-                .schedule();
-        }
+        backgroundJobManager.startImmediateFilesSyncJob(false, true);
 
         // retry failed uploads
         new Thread(() -> FileUploader.retryFailedUploads(

+ 6 - 35
src/main/java/com/owncloud/android/utils/FilesSyncHelper.java

@@ -31,7 +31,6 @@ import android.net.Uri;
 import android.os.Build;
 import android.provider.MediaStore;
 
-import com.evernote.android.job.JobManager;
 import com.evernote.android.job.JobRequest;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.core.Clock;
@@ -47,8 +46,6 @@ import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileUploader;
-import com.owncloud.android.jobs.FilesSyncJob;
-import com.owncloud.android.jobs.OfflineSyncJob;
 import com.owncloud.android.lib.common.utils.Log_OC;
 
 import org.lukhnos.nnio.file.FileVisitResult;
@@ -60,8 +57,6 @@ import org.lukhnos.nnio.file.attribute.BasicFileAttributes;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
 
 import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
 
@@ -79,7 +74,7 @@ public final class FilesSyncHelper {
         // utility class -> private constructor
     }
 
-    public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, boolean syncNow) {
+    private static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder) {
         final Context context = MainApp.getAppContext();
         final ContentResolver contentResolver = context.getContentResolver();
 
@@ -124,26 +119,19 @@ public final class FilesSyncHelper {
                     Log_OC.e(TAG, "Something went wrong while indexing files for auto upload", e);
                 }
             }
-
-            if (syncNow) {
-                new JobRequest.Builder(FilesSyncJob.TAG)
-                    .setExact(1_000L)
-                    .setUpdateCurrent(false)
-                    .build()
-                    .schedule();
-            }
         }
     }
 
-    public static void insertAllDBEntries(AppPreferences preferences, Clock clock, boolean skipCustom,
-                                          boolean syncNow) {
+    public static void insertAllDBEntries(AppPreferences preferences,
+                                          Clock clock,
+                                          boolean skipCustom) {
         final Context context = MainApp.getAppContext();
         final ContentResolver contentResolver = context.getContentResolver();
         SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver, preferences, clock);
 
         for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
             if (syncedFolder.isEnabled() && (!skipCustom || syncedFolder.getType() != MediaFolderType.CUSTOM)) {
-                insertAllDBEntriesForSyncedFolder(syncedFolder, syncNow);
+                insertAllDBEntriesForSyncedFolder(syncedFolder);
             }
         }
     }
@@ -233,27 +221,10 @@ public final class FilesSyncHelper {
     }
 
     public static void scheduleFilesSyncIfNeeded(Context context, BackgroundJobManager jobManager) {
-        // always run this because it also allows us to perform retries of manual uploads
-        new JobRequest.Builder(FilesSyncJob.TAG)
-                .setPeriodic(900000L, 300000L)
-                .setUpdateCurrent(true)
-                .build()
-                .schedule();
-
+        jobManager.schedulePeriodicFilesSyncJob();
         if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             jobManager.scheduleContentObserverJob();
         }
     }
-
-    public static void scheduleOfflineSyncIfNeeded() {
-        Set<JobRequest> jobRequests = JobManager.instance().getAllJobRequestsForTag(OfflineSyncJob.TAG);
-        if (jobRequests.isEmpty()) {
-            new JobRequest.Builder(OfflineSyncJob.TAG)
-                .setPeriodic(TimeUnit.MINUTES.toMillis(15), TimeUnit.MINUTES.toMillis(5))
-                .setUpdateCurrent(false)
-                .build()
-                .schedule();
-        }
-    }
 }
 

+ 1 - 1
src/main/res/layout/fragment_etm_background_jobs.xml

@@ -3,7 +3,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".etm.pages.EtmBackgroundJobsFragment">
+    tools:context="com.nextcloud.client.etm.pages.EtmBackgroundJobsFragment">
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/etm_background_jobs_list"

+ 6 - 0
src/main/res/menu/fragment_etm_background_jobs.xml

@@ -42,6 +42,12 @@
         app:showAsAction="never"
         android:showAsAction="never" />
 
+    <item
+        android:id="@+id/etm_background_jobs_schedule_test"
+        android:title="@string/etm_background_jobs_schedule_test_job"
+        app:showAsAction="never"
+        android:showAsAction="never" />
+
     <item
         android:id="@+id/etm_background_jobs_cancel_test"
         android:title="@string/etm_background_jobs_stop_test_job"

+ 1 - 0
src/main/res/values/strings.xml

@@ -895,6 +895,7 @@
     <string name="etm_background_jobs_cancel_all">Cancel all jobs</string>
     <string name="etm_background_jobs_prune">Prune inactive jobs</string>
     <string name="etm_background_jobs_start_test_job">Start test job</string>
+    <string name="etm_background_jobs_schedule_test_job">Schedule test job</string>
     <string name="etm_background_jobs_stop_test_job">Stop test job</string>
     <string name="etm_background_job_uuid">UUID</string>
     <string name="etm_background_job_name">Job name</string>