mirror of
https://github.com/nextcloud/android.git
synced 2024-11-27 09:39:25 +03:00
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:
parent
5da1b7f8a9
commit
77c4a3e6a5
23 changed files with 830 additions and 605 deletions
|
@ -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())
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt
Normal file
234
src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt
Normal file
143
src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue