Migrate file sync related jobs to WorkManager API

* FilesSyncJob.java converted to FilesSyncWork.kt
* FilesSyncJob removed from code
* Extended ETM screen to allow scheduling immediate
  TestJob (for development experimentation)
* OfflineSyncJob.java conveted to OfflineSyncWork.kt
* OfflineSyncJob removed from code
* Fixed re-scheduling of content observer work
* MediaFoldersDetectionJob.java converted to
  MediaFoldersDetectionWork.kt
* MediaFolderDetectionJob removed from code

Fixes #5881

Signed-off-by: Chris Narkiewicz <hello@ezaquarii.com>
This commit is contained in:
Chris Narkiewicz 2020-04-12 13:16:01 +01:00
parent 5da1b7f8a9
commit 77c4a3e6a5
No known key found for this signature in database
GPG key ID: 30D28CA4CCC665C6
23 changed files with 830 additions and 605 deletions

View file

@ -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())
)
}

View file

@ -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"

View file

@ -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() {

View file

@ -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

View file

@ -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
)
}
}

View file

@ -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()

View file

@ -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.KEEP, request)
workManager.enqueueUniquePeriodicWork(JOB_TEST, ExistingPeriodicWorkPolicy.REPLACE, request)
}
override fun startImmediateTestJob() {
val request = oneTimeRequestBuilder(TestJob::class, JOB_TEST)
.build()
workManager.enqueueUniqueWork(JOB_TEST, ExistingWorkPolicy.REPLACE, request)
}
override fun cancelTestJob() {

View file

@ -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,

View file

@ -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())

View file

@ -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()
}
}

View file

@ -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, backgroundJobManager, skipCustom, false)
// Create all the providers we'll need
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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
@ -311,18 +305,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();
}
@ -465,7 +449,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
final UserAccountManager accountManager,
final ConnectivityService connectivityService,
final PowerManagementService powerManagementService,
final BackgroundJobManager jobManager,
final BackgroundJobManager backgroundJobManager,
final Clock clock
) {
updateToAutoUpload();
@ -482,15 +466,16 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
}
}
initiateExistingAutoUploadEntries(clock);
initiateExistingAutoUploadEntries(backgroundJobManager, clock);
FilesSyncHelper.scheduleFilesSyncIfNeeded(mContext, jobManager);
FilesSyncHelper.scheduleFilesSyncIfNeeded(mContext, backgroundJobManager);
FilesSyncHelper.restartJobsIfNeeded(
uploadsStorageManager,
accountManager,
connectivityService,
powerManagementService);
FilesSyncHelper.scheduleOfflineSyncIfNeeded();
backgroundJobManager.scheduleOfflineSync();
ReceiversHelper.registerNetworkChangeReceiver(uploadsStorageManager,
accountManager,
@ -755,7 +740,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
}
}
private static void initiateExistingAutoUploadEntries(Clock clock) {
private static void initiateExistingAutoUploadEntries(BackgroundJobManager backgroundJobManager, Clock clock) {
new Thread(() -> {
AppPreferences preferences = AppPreferencesImpl.fromContext(getAppContext());
if (!preferences.isAutoUploadInitialized()) {
@ -764,7 +749,9 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
if (syncedFolder.isEnabled()) {
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, true);
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(backgroundJobManager,
syncedFolder,
true);
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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,9 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
}
if (syncedFolderDisplayItem.isEnabled()) {
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolderDisplayItem, true);
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(backgroundJobManager,
syncedFolderDisplayItem,
true);
showBatteryOptimizationInfo();
}
}
@ -779,7 +782,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);
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(backgroundJobManager, item, true);
} else {
String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
@ -797,7 +800,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
if (storedId != -1) {
item.setId(storedId);
if (item.isEnabled()) {
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item, true);
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(backgroundJobManager, item, true);
} else {
String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);

View file

@ -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
@ -181,19 +184,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(

View file

@ -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,9 @@ public final class FilesSyncHelper {
// utility class -> private constructor
}
public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, boolean syncNow) {
public static void insertAllDBEntriesForSyncedFolder(BackgroundJobManager backgroundJobManager,
SyncedFolder syncedFolder,
boolean syncNow) {
final Context context = MainApp.getAppContext();
final ContentResolver contentResolver = context.getContentResolver();
@ -126,16 +123,15 @@ public final class FilesSyncHelper {
}
if (syncNow) {
new JobRequest.Builder(FilesSyncJob.TAG)
.setExact(1_000L)
.setUpdateCurrent(false)
.build()
.schedule();
backgroundJobManager.startImmediateFilesSyncJob(false, false);
}
}
}
public static void insertAllDBEntries(AppPreferences preferences, Clock clock, boolean skipCustom,
public static void insertAllDBEntries(AppPreferences preferences,
Clock clock,
BackgroundJobManager backgroundJobManager,
boolean skipCustom,
boolean syncNow) {
final Context context = MainApp.getAppContext();
final ContentResolver contentResolver = context.getContentResolver();
@ -143,7 +139,7 @@ public final class FilesSyncHelper {
for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
if (syncedFolder.isEnabled() && (!skipCustom || syncedFolder.getType() != MediaFolderType.CUSTOM)) {
insertAllDBEntriesForSyncedFolder(syncedFolder, syncNow);
insertAllDBEntriesForSyncedFolder(backgroundJobManager, syncedFolder, syncNow);
}
}
}
@ -233,27 +229,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();
}
}
}

View file

@ -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"

View file

@ -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"

View file

@ -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>