added DEV version to repo
This commit is contained in:
parent
1ef725ef20
commit
23e673bfdf
2135 changed files with 97033 additions and 21206 deletions
|
|
@ -2,13 +2,14 @@
|
|||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||
*/
|
||||
package com.nextcloud.client.jobs
|
||||
|
||||
import android.provider.MediaStore
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
|
|
@ -26,11 +27,18 @@ import com.nextcloud.client.core.Clock
|
|||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
|
||||
import com.nextcloud.client.jobs.download.FileDownloadWorker
|
||||
import com.nextcloud.client.jobs.metadata.MetadataWorker
|
||||
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker
|
||||
import com.nextcloud.client.preferences.AppPreferences
|
||||
import com.nextcloud.utils.extensions.isWorkRunning
|
||||
import com.nextcloud.utils.extensions.isWorkScheduled
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.operations.DownloadType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
@ -55,10 +63,10 @@ internal class BackgroundJobManagerImpl(
|
|||
private val workManager: WorkManager,
|
||||
private val clock: Clock,
|
||||
private val preferences: AppPreferences
|
||||
) : BackgroundJobManager, Injectable {
|
||||
) : BackgroundJobManager,
|
||||
Injectable {
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_ALL = "*" // This tag allows us to retrieve list of all jobs run by Nextcloud client
|
||||
const val JOB_CONTENT_OBSERVER = "content_observer"
|
||||
const val JOB_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup"
|
||||
|
|
@ -79,9 +87,12 @@ internal class BackgroundJobManagerImpl(
|
|||
const val JOB_PDF_GENERATION = "pdf_generation"
|
||||
const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
|
||||
const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
|
||||
|
||||
const val JOB_OFFLINE_OPERATIONS = "offline_operations"
|
||||
const val JOB_PERIODIC_OFFLINE_OPERATIONS = "periodic_offline_operations"
|
||||
const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
|
||||
const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"
|
||||
const val JOB_METADATA_SYNC = "metadata_sync"
|
||||
const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync"
|
||||
|
||||
const val JOB_TEST = "test_job"
|
||||
|
||||
|
|
@ -95,16 +106,16 @@ internal class BackgroundJobManagerImpl(
|
|||
const val NOT_SET_VALUE = "not set"
|
||||
const val PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L
|
||||
const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L
|
||||
const val OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES = 5L
|
||||
const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L
|
||||
const val DEFAULT_BACKOFF_CRITERIA_DELAY_SEC = 300L
|
||||
|
||||
private const val KEEP_LOG_MILLIS = 1000 * 60 * 60 * 24 * 3L
|
||||
|
||||
fun formatNameTag(name: String, user: User? = null): String {
|
||||
return if (user == null) {
|
||||
"$TAG_PREFIX_NAME:$name"
|
||||
} else {
|
||||
"$TAG_PREFIX_NAME:$name ${user.accountName}"
|
||||
}
|
||||
fun formatNameTag(name: String, user: User? = null): String = if (user == null) {
|
||||
"$TAG_PREFIX_NAME:$name"
|
||||
} else {
|
||||
"$TAG_PREFIX_NAME:$name ${user.accountName}"
|
||||
}
|
||||
|
||||
fun formatUserTag(user: User): String = "$TAG_PREFIX_USER:${user.accountName}"
|
||||
|
|
@ -121,36 +132,32 @@ internal class BackgroundJobManagerImpl(
|
|||
}
|
||||
}
|
||||
|
||||
fun parseTimestamp(timestamp: String): Date {
|
||||
return try {
|
||||
val ms = timestamp.toLong()
|
||||
Date(ms)
|
||||
} catch (ex: NumberFormatException) {
|
||||
Date(0)
|
||||
}
|
||||
fun parseTimestamp(timestamp: String): Date = try {
|
||||
val ms = timestamp.toLong()
|
||||
Date(ms)
|
||||
} catch (ex: NumberFormatException) {
|
||||
Date(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert platform [androidx.work.WorkInfo] object into application-specific [JobInfo] model.
|
||||
* Conversion extracts work metadata from tags.
|
||||
*/
|
||||
fun fromWorkInfo(info: WorkInfo?): JobInfo? {
|
||||
return if (info != null) {
|
||||
val metadata = mutableMapOf<String, String>()
|
||||
info.tags.forEach { parseTag(it)?.let { metadata[it.first] = it.second } }
|
||||
val timestamp = parseTimestamp(metadata.get(TAG_PREFIX_START_TIMESTAMP) ?: "0")
|
||||
JobInfo(
|
||||
id = info.id,
|
||||
state = info.state.toString(),
|
||||
name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE,
|
||||
user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE,
|
||||
started = timestamp,
|
||||
progress = info.progress.getInt("progress", -1),
|
||||
workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
fun fromWorkInfo(info: WorkInfo?): JobInfo? = if (info != null) {
|
||||
val metadata = mutableMapOf<String, String>()
|
||||
info.tags.forEach { parseTag(it)?.let { metadata[it.first] = it.second } }
|
||||
val timestamp = parseTimestamp(metadata.get(TAG_PREFIX_START_TIMESTAMP) ?: "0")
|
||||
JobInfo(
|
||||
id = info.id,
|
||||
state = info.state.toString(),
|
||||
name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE,
|
||||
user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE,
|
||||
started = timestamp,
|
||||
progress = info.progress.getInt("progress", -1),
|
||||
workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun deleteOldLogs(logEntries: MutableList<LogEntry>): MutableList<LogEntry> {
|
||||
|
|
@ -168,6 +175,8 @@ internal class BackgroundJobManagerImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private val defaultDispatcherScope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override fun logStartOfWorker(workerName: String?) {
|
||||
val logs = deleteOldLogs(preferences.readLogEntry().toMutableList())
|
||||
|
||||
|
|
@ -195,13 +204,15 @@ internal class BackgroundJobManagerImpl(
|
|||
private fun oneTimeRequestBuilder(
|
||||
jobClass: KClass<out ListenableWorker>,
|
||||
jobName: String,
|
||||
user: User? = null
|
||||
user: User? = null,
|
||||
constraints: Constraints = Constraints.Builder().build()
|
||||
): OneTimeWorkRequest.Builder {
|
||||
val builder = OneTimeWorkRequest.Builder(jobClass.java)
|
||||
.addTag(TAG_ALL)
|
||||
.addTag(formatNameTag(jobName, user))
|
||||
.addTag(formatTimeTag(clock.currentTime))
|
||||
.addTag(formatClassTag(jobClass))
|
||||
.setConstraints(constraints)
|
||||
user?.let { builder.addTag(formatUserTag(it)) }
|
||||
return builder
|
||||
}
|
||||
|
|
@ -214,7 +225,8 @@ internal class BackgroundJobManagerImpl(
|
|||
jobName: String,
|
||||
intervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
|
||||
flexIntervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
|
||||
user: User? = null
|
||||
user: User? = null,
|
||||
constraints: Constraints = Constraints.Builder().build()
|
||||
): PeriodicWorkRequest.Builder {
|
||||
val builder = PeriodicWorkRequest.Builder(
|
||||
jobClass.java,
|
||||
|
|
@ -227,6 +239,7 @@ internal class BackgroundJobManagerImpl(
|
|||
.addTag(formatNameTag(jobName, user))
|
||||
.addTag(formatTimeTag(clock.currentTime))
|
||||
.addTag(formatClassTag(jobClass))
|
||||
.setConstraints(constraints)
|
||||
user?.let { builder.addTag(formatUserTag(it)) }
|
||||
return builder
|
||||
}
|
||||
|
|
@ -298,13 +311,13 @@ internal class BackgroundJobManagerImpl(
|
|||
contactsAccountName: String?,
|
||||
contactsAccountType: String?,
|
||||
vCardFilePath: String,
|
||||
selectedContacts: IntArray
|
||||
selectedContactsFilePath: String
|
||||
): LiveData<JobInfo?> {
|
||||
val data = Data.Builder()
|
||||
.putString(ContactsImportWork.ACCOUNT_NAME, contactsAccountName)
|
||||
.putString(ContactsImportWork.ACCOUNT_TYPE, contactsAccountType)
|
||||
.putString(ContactsImportWork.VCARD_FILE_PATH, vCardFilePath)
|
||||
.putIntArray(ContactsImportWork.SELECTED_CONTACTS_INDICES, selectedContacts)
|
||||
.putString(ContactsImportWork.SELECTED_CONTACTS_FILE_PATH, selectedContactsFilePath)
|
||||
.build()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
|
|
@ -403,32 +416,134 @@ internal class BackgroundJobManagerImpl(
|
|||
workManager.cancelJob(JOB_PERIODIC_CALENDAR_BACKUP, user)
|
||||
}
|
||||
|
||||
override fun schedulePeriodicFilesSyncJob() {
|
||||
override fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean =
|
||||
workManager.isWorkRunning(JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID) &&
|
||||
workManager.isWorkRunning(JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID)
|
||||
|
||||
override fun startPeriodicallyOfflineOperation() {
|
||||
val inputData = Data.Builder()
|
||||
.putString(OfflineOperationsWorker.JOB_NAME, JOB_PERIODIC_OFFLINE_OPERATIONS)
|
||||
.build()
|
||||
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = OfflineOperationsWorker::class,
|
||||
jobName = JOB_PERIODIC_OFFLINE_OPERATIONS,
|
||||
intervalMins = OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES
|
||||
)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
JOB_PERIODIC_OFFLINE_OPERATIONS,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun startOfflineOperations() {
|
||||
val inputData = Data.Builder()
|
||||
.putString(OfflineOperationsWorker.JOB_NAME, JOB_OFFLINE_OPERATIONS)
|
||||
.build()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
// Backoff criteria define how the system should retry the task if it fails.
|
||||
// LINEAR means each retry will be delayed linearly (e.g., 10s, 20s, 30s...)
|
||||
// DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES is used as the initial delay duration.
|
||||
val backoffCriteriaPolicy = BackoffPolicy.LINEAR
|
||||
val backoffCriteriaDelay = DEFAULT_BACKOFF_CRITERIA_DELAY_SEC
|
||||
|
||||
val request =
|
||||
oneTimeRequestBuilder(OfflineOperationsWorker::class, JOB_OFFLINE_OPERATIONS, constraints = constraints)
|
||||
.setBackoffCriteria(
|
||||
backoffCriteriaPolicy,
|
||||
backoffCriteriaDelay,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
JOB_OFFLINE_OPERATIONS,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun schedulePeriodicFilesSyncJob(syncedFolderID: Long) {
|
||||
val arguments = Data.Builder()
|
||||
.putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
.build()
|
||||
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = FilesSyncWork::class,
|
||||
jobName = JOB_PERIODIC_FILES_SYNC,
|
||||
jobName = JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID,
|
||||
intervalMins = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES
|
||||
).build()
|
||||
workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||
)
|
||||
.setInputData(arguments)
|
||||
.build()
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID,
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun startImmediateFilesSyncJob(
|
||||
syncedFolderID: Long,
|
||||
overridePowerSaving: Boolean,
|
||||
changedFiles: Array<String>
|
||||
changedFiles: Array<String?>
|
||||
) {
|
||||
val arguments = Data.Builder()
|
||||
.putBoolean(FilesSyncWork.OVERRIDE_POWER_SAVING, overridePowerSaving)
|
||||
.putStringArray(FilesSyncWork.CHANGED_FILES, changedFiles)
|
||||
.putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)
|
||||
.build()
|
||||
|
||||
val request = oneTimeRequestBuilder(
|
||||
jobClass = FilesSyncWork::class,
|
||||
jobName = JOB_IMMEDIATE_FILES_SYNC
|
||||
jobName = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID
|
||||
)
|
||||
.setInputData(arguments)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(JOB_IMMEDIATE_FILES_SYNC, ExistingWorkPolicy.APPEND, request)
|
||||
workManager.enqueueUniqueWork(
|
||||
JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID,
|
||||
ExistingWorkPolicy.APPEND,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun cancelTwoWaySyncJob() {
|
||||
workManager.cancelJob(JOB_INTERNAL_TWO_WAY_SYNC)
|
||||
}
|
||||
|
||||
override fun cancelAllFilesDownloadJobs() {
|
||||
workManager.cancelAllWorkByTag(formatClassTag(FileDownloadWorker::class))
|
||||
}
|
||||
|
||||
override fun startMetadataSyncJob(currentDirPath: String) {
|
||||
val inputData = Data.Builder()
|
||||
.putString(MetadataWorker.FILE_PATH, currentDirPath)
|
||||
.build()
|
||||
|
||||
val constrains = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
||||
val request = oneTimeRequestBuilder(MetadataWorker::class, JOB_METADATA_SYNC)
|
||||
.setConstraints(constrains)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
JOB_METADATA_SYNC,
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun scheduleOfflineSync() {
|
||||
|
|
@ -491,34 +606,75 @@ internal class BackgroundJobManagerImpl(
|
|||
workManager.enqueue(request)
|
||||
}
|
||||
|
||||
private fun startFileUploadJobTag(user: User): String {
|
||||
return JOB_FILES_UPLOAD + user.accountName
|
||||
private fun startFileUploadJobTag(user: User): String = JOB_FILES_UPLOAD + user.accountName
|
||||
|
||||
override fun isStartFileUploadJobScheduled(user: User): Boolean =
|
||||
workManager.isWorkScheduled(startFileUploadJobTag(user))
|
||||
|
||||
/**
|
||||
* This method supports initiating uploads for various scenarios, including:
|
||||
* - New upload batches
|
||||
* - Failed uploads
|
||||
* - FilesSyncWork
|
||||
* - ...
|
||||
*
|
||||
* @param user The user for whom the upload job is being created.
|
||||
* @param uploadIds Array of upload IDs to be processed. These IDs originate from multiple sources
|
||||
* and cannot be determined directly from the account name or a single function
|
||||
* within the worker.
|
||||
*/
|
||||
override fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean) {
|
||||
defaultDispatcherScope.launch {
|
||||
val batchSize = FileUploadHelper.MAX_FILE_COUNT
|
||||
val batches = uploadIds.toList().chunked(batchSize)
|
||||
val tag = startFileUploadJobTag(user)
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val dataBuilder = Data.Builder()
|
||||
.putBoolean(
|
||||
FileUploadWorker.SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION,
|
||||
showSameFileAlreadyExistsNotification
|
||||
)
|
||||
.putString(FileUploadWorker.ACCOUNT, user.accountName)
|
||||
.putInt(FileUploadWorker.TOTAL_UPLOAD_SIZE, uploadIds.size)
|
||||
|
||||
val workRequests = batches.mapIndexed { index, batch ->
|
||||
dataBuilder
|
||||
.putLongArray(FileUploadWorker.UPLOAD_IDS, batch.toLongArray())
|
||||
.putInt(FileUploadWorker.CURRENT_BATCH_INDEX, index)
|
||||
|
||||
oneTimeRequestBuilder(FileUploadWorker::class, JOB_FILES_UPLOAD, user)
|
||||
.addTag(tag)
|
||||
.setInputData(dataBuilder.build())
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
}
|
||||
|
||||
// Chain the work requests sequentially
|
||||
if (workRequests.isNotEmpty()) {
|
||||
var workChain = workManager.beginUniqueWork(
|
||||
tag,
|
||||
ExistingWorkPolicy.APPEND_OR_REPLACE,
|
||||
workRequests.first()
|
||||
)
|
||||
|
||||
workRequests.drop(1).forEach { request ->
|
||||
workChain = workChain.then(request)
|
||||
}
|
||||
|
||||
workChain.enqueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isStartFileUploadJobScheduled(user: User): Boolean {
|
||||
return workManager.isWorkScheduled(startFileUploadJobTag(user))
|
||||
}
|
||||
private fun startFileDownloadJobTag(user: User, fileId: Long): String =
|
||||
JOB_FOLDER_DOWNLOAD + user.accountName + fileId
|
||||
|
||||
override fun startFilesUploadJob(user: User) {
|
||||
val data = workDataOf(FileUploadWorker.ACCOUNT to user.accountName)
|
||||
|
||||
val tag = startFileUploadJobTag(user)
|
||||
|
||||
val request = oneTimeRequestBuilder(FileUploadWorker::class, JOB_FILES_UPLOAD, user)
|
||||
.addTag(tag)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
private fun startFileDownloadJobTag(user: User, fileId: Long): String {
|
||||
return JOB_FOLDER_DOWNLOAD + user.accountName + fileId
|
||||
}
|
||||
|
||||
override fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean {
|
||||
return workManager.isWorkScheduled(startFileDownloadJobTag(user, fileId))
|
||||
}
|
||||
override fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean =
|
||||
workManager.isWorkScheduled(startFileDownloadJobTag(user, fileId))
|
||||
|
||||
override fun startFileDownloadJob(
|
||||
user: User,
|
||||
|
|
@ -546,7 +702,9 @@ internal class BackgroundJobManagerImpl(
|
|||
.setInputData(data)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.REPLACE, request)
|
||||
// Since for each file new FileDownloadWorker going to be scheduled,
|
||||
// better to use ExistingWorkPolicy.KEEP policy.
|
||||
workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
override fun getFileUploads(user: User): LiveData<List<JobInfo>> {
|
||||
|
|
@ -625,4 +783,16 @@ internal class BackgroundJobManagerImpl(
|
|||
request
|
||||
)
|
||||
}
|
||||
|
||||
override fun scheduleInternal2WaySync(intervalMinutes: Long) {
|
||||
val request = periodicRequestBuilder(
|
||||
jobClass = InternalTwoWaySyncWork::class,
|
||||
jobName = JOB_INTERNAL_TWO_WAY_SYNC,
|
||||
intervalMins = intervalMinutes
|
||||
)
|
||||
.setInitialDelay(intervalMinutes, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue