blob: 8f33b1d6ce4a04eda2b87428603e6cad28bee9aa [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.work
import android.os.Build
import android.util.Log
import androidx.annotation.IntRange
import androidx.annotation.RestrictTo
import androidx.core.util.Consumer
import androidx.work.impl.DefaultRunnableScheduler
import androidx.work.impl.Scheduler
import androidx.work.impl.utils.INITIAL_ID
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.asExecutor
/**
* The Configuration object used to customize [WorkManager] upon initialization.
* Configuration contains various parameters used to setup WorkManager. For example, it is possible
* to customize the [Executor] used by [Worker]s here.
*
* To set a custom Configuration for WorkManager, see [WorkManager.initialize].
*/
class Configuration internal constructor(builder: Builder) {
/**
* The [Executor] used by [WorkManager] to execute [Worker]s.
*/
val executor: Executor
/**
* The [CoroutineContext] used by [WorkManager] to execute [CoroutineWorker]s.
*/
val workerCoroutineContext: CoroutineContext
/**
* The [Executor] used by [WorkManager] for all its internal business logic
*/
val taskExecutor: Executor
/**
* The [Clock] used by [WorkManager] to calculate schedules and perform book-keeping.
*/
val clock: Clock
/**
* The [WorkerFactory] used by [WorkManager] to create [ListenableWorker]s
*/
val workerFactory: WorkerFactory
/**
* The [InputMergerFactory] used by [WorkManager] to create instances of
* [InputMerger]s.
*/
val inputMergerFactory: InputMergerFactory
/**
* The [RunnableScheduler] to keep track of timed work in the in-process
* scheduler.
*/
val runnableScheduler: RunnableScheduler
/**
* The exception handler that is used to intercept
* exceptions caused when trying to initialize [WorkManager].
*/
val initializationExceptionHandler: Consumer<Throwable>?
/**
* The exception handler that can be used to intercept exceptions
* caused when trying to schedule [WorkRequest]s.
*/
val schedulingExceptionHandler: Consumer<Throwable>?
/**
* The exception handler that can be used to intercept exceptions
* caused when trying to initialize [ListenableWorker]s.
*/
val workerInitializationExceptionHandler: Consumer<WorkerExceptionInfo>?
/**
* The exception handler that can be used to intercept exceptions
* caused when trying to execute [ListenableWorker]s.
*/
val workerExecutionExceptionHandler: Consumer<WorkerExceptionInfo>?
/**
* The [String] name of the process where work should be scheduled.
*/
val defaultProcessName: String?
/**
* The minimum logging level, corresponding to the constants found in [android.util.Log]
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val minimumLoggingLevel: Int
/**
* The first valid id (inclusive) used by [WorkManager] when creating new
* instances of [android.app.job.JobInfo]s.
*
* If the current `jobId` goes beyond the bounds of the defined range of
* ([Configuration.minJobSchedulerId], [Configuration.maxJobSchedulerId]), it is reset to
* ([Configuration.minJobSchedulerId]).
*/
val minJobSchedulerId: Int
/**
* The last valid id (inclusive) used by [WorkManager] when
* creating new instances of [android.app.job.JobInfo]s.
*
* If the current `jobId` goes beyond the bounds of the defined range of
* ([Configuration.minJobSchedulerId], [Configuration.maxJobSchedulerId]), it is reset to
* ([Configuration.minJobSchedulerId]).
*/
val maxJobSchedulerId: Int
/**
* Maximum number of Workers with [Constraints.contentUriTriggers] that
* could be enqueued simultaneously.
*
* Unlike the other workers Workers with [Constraints.contentUriTriggers] must immediately
* occupy slots in JobScheduler to avoid missing updates, thus they are separated in the
* its own category.
*/
val contentUriTriggerWorkersLimit: Int
/**
* The maximum number of system requests which can be enqueued by [WorkManager]
* when using [android.app.job.JobScheduler] or [android.app.AlarmManager]
*/
@get:IntRange(from = MIN_SCHEDULER_LIMIT.toLong(), to = Scheduler.MAX_SCHEDULER_LIMIT.toLong())
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val maxSchedulerLimit: Int
/**
* @return `true` If the default task [Executor] is being used
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val isUsingDefaultTaskExecutor: Boolean
init {
val builderWorkerDispatcher = builder.workerContext
executor = builder.executor ?: builderWorkerDispatcher?.asExecutor()
?: createDefaultExecutor(isTaskExecutor = false)
workerCoroutineContext = when {
builderWorkerDispatcher != null -> builderWorkerDispatcher
// we don't want simply always use executor.asCoroutineDispatcher()
// as compatibility measure
builder.executor != null -> executor.asCoroutineDispatcher()
else -> Dispatchers.Default
}
isUsingDefaultTaskExecutor = builder.taskExecutor == null
// This executor is used for *both* WorkManager's tasks and Room's query executor.
// So this should not be a single threaded executor. Writes will still be serialized
// as this will be wrapped with an SerialExecutor.
taskExecutor = builder.taskExecutor ?: createDefaultExecutor(isTaskExecutor = true)
clock = builder.clock ?: SystemClock()
workerFactory = builder.workerFactory ?: DefaultWorkerFactory
inputMergerFactory = builder.inputMergerFactory ?: NoOpInputMergerFactory
runnableScheduler = builder.runnableScheduler ?: DefaultRunnableScheduler()
minimumLoggingLevel = builder.loggingLevel
minJobSchedulerId = builder.minJobSchedulerId
maxJobSchedulerId = builder.maxJobSchedulerId
maxSchedulerLimit = if (Build.VERSION.SDK_INT == 23) {
// We double schedule jobs in SDK 23. So use half the number of max slots specified.
builder.maxSchedulerLimit / 2
} else {
builder.maxSchedulerLimit
}
initializationExceptionHandler = builder.initializationExceptionHandler
schedulingExceptionHandler = builder.schedulingExceptionHandler
workerInitializationExceptionHandler = builder.workerInitializationExceptionHandler
workerExecutionExceptionHandler = builder.workerExecutionExceptionHandler
defaultProcessName = builder.defaultProcessName
contentUriTriggerWorkersLimit = builder.contentUriTriggerWorkersLimit
}
/**
* A Builder for [Configuration]s.
*/
class Builder {
internal var executor: Executor? = null
internal var workerContext: CoroutineContext? = null
internal var workerFactory: WorkerFactory? = null
internal var inputMergerFactory: InputMergerFactory? = null
internal var taskExecutor: Executor? = null
internal var clock: Clock? = null
internal var runnableScheduler: RunnableScheduler? = null
internal var initializationExceptionHandler: Consumer<Throwable>? = null
internal var schedulingExceptionHandler: Consumer<Throwable>? = null
internal var workerInitializationExceptionHandler: Consumer<WorkerExceptionInfo>? = null
internal var workerExecutionExceptionHandler: Consumer<WorkerExceptionInfo>? = null
internal var defaultProcessName: String? = null
internal var loggingLevel: Int = Log.INFO
internal var minJobSchedulerId: Int = INITIAL_ID
internal var maxJobSchedulerId: Int = Int.MAX_VALUE
internal var maxSchedulerLimit: Int = MIN_SCHEDULER_LIMIT
internal var contentUriTriggerWorkersLimit: Int = DEFAULT_CONTENT_URI_TRIGGERS_WORKERS_LIMIT
/**
* Creates a new [Configuration.Builder].
*/
constructor()
/**
* Creates a new [Configuration.Builder] with an existing [Configuration] as its
* template.
*
* @param configuration An existing [Configuration] to use as a template
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
constructor(configuration: Configuration) {
// Note that these must be accessed through fields and not the getters, which can
// otherwise manipulate the returned value (see getMaxSchedulerLimit(), for example).
executor = configuration.executor
workerFactory = configuration.workerFactory
inputMergerFactory = configuration.inputMergerFactory
taskExecutor = configuration.taskExecutor
clock = configuration.clock
loggingLevel = configuration.minimumLoggingLevel
minJobSchedulerId = configuration.minJobSchedulerId
maxJobSchedulerId = configuration.maxJobSchedulerId
maxSchedulerLimit = configuration.maxSchedulerLimit
runnableScheduler = configuration.runnableScheduler
initializationExceptionHandler = configuration.initializationExceptionHandler
schedulingExceptionHandler = configuration.schedulingExceptionHandler
workerInitializationExceptionHandler =
configuration.workerInitializationExceptionHandler
workerExecutionExceptionHandler = configuration.workerExecutionExceptionHandler
defaultProcessName = configuration.defaultProcessName
}
/**
* Specifies a custom [WorkerFactory] for WorkManager.
*
* @param workerFactory A [WorkerFactory] for creating [ListenableWorker]s
* @return This [Builder] instance
*/
fun setWorkerFactory(workerFactory: WorkerFactory): Builder {
this.workerFactory = workerFactory
return this
}
/**
* Specifies a custom [InputMergerFactory] for WorkManager.
*
* @param inputMergerFactory A [InputMergerFactory] for creating [InputMerger]s
* @return This [Builder] instance
*/
fun setInputMergerFactory(inputMergerFactory: InputMergerFactory): Builder {
this.inputMergerFactory = inputMergerFactory
return this
}
/**
* Specifies a custom [Executor] to run [Worker.doWork].
*
* If [setWorkerCoroutineContext] wasn't called then the [executor] will be used as
* [CoroutineDispatcher] to run [CoroutineWorker] as well.
*
* @param executor An [Executor] for running [Worker]s
* @return This [Builder] instance
*/
fun setExecutor(executor: Executor): Builder {
this.executor = executor
return this
}
/**
* Specifies a custom [CoroutineDispatcher] to run [CoroutineWorker.doWork].
*
* If [setExecutor] wasn't called then [dispatcher] will be used as [Executor]
* to run [Worker] as well.
*
* @param dispatcher A [CoroutineDispatcher] for running [CoroutineWorker]s
* @return This [Builder] instance
*/
fun setWorkerCoroutineContext(dispatcher: CoroutineDispatcher): Builder {
this.workerContext = dispatcher
return this
}
/**
* Specifies a [Executor] which will be used by WorkManager for all its
* internal book-keeping.
*
* For best performance this [Executor] should be bounded.
*
* For more information look at [androidx.room.RoomDatabase.Builder.setQueryExecutor].
*
* @param taskExecutor The [Executor] which will be used by WorkManager for
* all its internal book-keeping
* @return This [Builder] instance
*/
fun setTaskExecutor(taskExecutor: Executor): Builder {
this.taskExecutor = taskExecutor
return this
}
/**
* Sets a [Clock] for WorkManager to calculate schedules and perform book-keeping.
*
* This should only be overridden for testing. It must return the same value as
* [System.currentTimeMillis] in production code.
*
* @param clock The [Clock] to use
* @return This [Builder] instance
*/
fun setClock(clock: Clock): Builder {
this.clock = clock
return this
}
/**
* Specifies the range of [android.app.job.JobInfo] IDs that can be used by
* [WorkManager]. WorkManager needs a range of at least `1000` IDs.
*
* JobScheduler uses integers as identifiers for jobs, and WorkManager delegates to
* JobScheduler on certain API levels. In order to not clash job codes used in the rest of
* your app, you can use this method to tell WorkManager the valid range of job IDs that it
* can use.
*
* The default values are `0` and `Integer#MAX_VALUE`.
*
* @param minJobSchedulerId The first valid [android.app.job.JobInfo] ID (inclusive).
* @param maxJobSchedulerId The last valid [android.app.job.JobInfo] ID (inclusive).
* @return This [Builder] instance
* @throws IllegalArgumentException when the size of the range is less than 1000
*/
fun setJobSchedulerJobIdRange(minJobSchedulerId: Int, maxJobSchedulerId: Int): Builder {
require(maxJobSchedulerId - minJobSchedulerId >= 1000) {
"WorkManager needs a range of at least 1000 job ids."
}
this.minJobSchedulerId = minJobSchedulerId
this.maxJobSchedulerId = maxJobSchedulerId
return this
}
/**
* Specifies the maximum number of system requests made by [WorkManager]
* when using [android.app.job.JobScheduler] or [android.app.AlarmManager].
*
* By default, WorkManager might schedule a large number of alarms or JobScheduler
* jobs. If your app uses JobScheduler or AlarmManager directly, this might exhaust the
* OS-enforced limit on the number of jobs or alarms an app is allowed to schedule. To help
* manage this situation, you can use this method to reduce the number of underlying jobs
* and alarms that WorkManager might schedule.
*
* When the application exceeds this limit, WorkManager maintains an internal queue of
* [WorkRequest]s, and schedules them when slots become free.
*
* WorkManager requires a minimum of [Configuration.MIN_SCHEDULER_LIMIT] slots; this
* is also the default value. The total number of slots also cannot exceed `50`.
*
* @param maxSchedulerLimit The total number of jobs which can be enqueued by
* [WorkManager] when using [android.app.job.JobScheduler].
* @return This [Builder] instance
* @throws IllegalArgumentException if `maxSchedulerLimit` is less than
* [Configuration.MIN_SCHEDULER_LIMIT]
*/
fun setMaxSchedulerLimit(maxSchedulerLimit: Int): Builder {
require(maxSchedulerLimit >= MIN_SCHEDULER_LIMIT) {
"WorkManager needs to be able to schedule at least 20 jobs in JobScheduler."
}
this.maxSchedulerLimit = min(maxSchedulerLimit, Scheduler.MAX_SCHEDULER_LIMIT)
return this
}
/**
* Specifies the maximum number of Workers with [Constraints.contentUriTriggers] that
* could be enqueued simultaneously.
*
* Unlike the other workers Workers with [Constraints.contentUriTriggers] must immediately
* occupy slots in JobScheduler to avoid missing updates, thus they are separated in the
* its own category.
*/
fun setContentUriTriggerWorkersLimit(contentUriTriggerWorkersLimit: Int): Builder {
this.contentUriTriggerWorkersLimit = max(contentUriTriggerWorkersLimit, 0)
return this
}
/**
* Specifies the minimum logging level, corresponding to the constants found in
* [android.util.Log]. For example, specifying [android.util.Log.VERBOSE] will
* log everything, whereas specifying [android.util.Log.ERROR] will only log errors
* and assertions.The default value is [android.util.Log.INFO].
*
* @param loggingLevel The minimum logging level, corresponding to the constants found in
* [android.util.Log]
* @return This [Builder] instance
*/
fun setMinimumLoggingLevel(loggingLevel: Int): Builder {
this.loggingLevel = loggingLevel
return this
}
/**
* Specifies the [RunnableScheduler] to be used by [WorkManager].
*
* This is used by the in-process scheduler to keep track of timed work.
*
* @param runnableScheduler The [RunnableScheduler] to be used
* @return This [Builder] instance
*/
fun setRunnableScheduler(runnableScheduler: RunnableScheduler): Builder {
this.runnableScheduler = runnableScheduler
return this
}
/**
* Specifies a `Consumer<Throwable>` that can be used to intercept
* exceptions caused when trying to initialize {@link WorkManager}, that usually happens
* when WorkManager cannot access its internal datastore.
*
* This exception handler will be invoked on a thread bound to
* [Configuration.taskExecutor].
*
* @param exceptionHandler an instance to handle exceptions
* @return This [Builder] instance
*/
fun setInitializationExceptionHandler(exceptionHandler: Consumer<Throwable>): Builder {
this.initializationExceptionHandler = exceptionHandler
return this
}
/**
* Specifies a `Consumer<Throwable>` that can be used to intercept
* exceptions caused when trying to schedule [WorkRequest]s.
*
* It allows the application to handle a [Throwable] throwable typically
* caused when trying to schedule [WorkRequest]s.
*
* This exception handler will be invoked on a thread bound to
* [Configuration.taskExecutor].
*
* @param schedulingExceptionHandler an instance to handle exceptions
* @return This [Builder] instance
*/
fun setSchedulingExceptionHandler(
schedulingExceptionHandler: Consumer<Throwable>
): Builder {
this.schedulingExceptionHandler = schedulingExceptionHandler
return this
}
/**
* Specifies a `WorkerExceptionHandler` that can be used to intercept
* exceptions caused when trying to initialize [ListenableWorker]s.
*
* This exception handler will be invoked on a thread bound to
* [Configuration.taskExecutor].
*
* @param workerExceptionHandler an instance to handle exceptions
* @return This [Builder] instance
*/
fun setWorkerInitializationExceptionHandler(
workerExceptionHandler: Consumer<WorkerExceptionInfo>
): Builder {
this.workerInitializationExceptionHandler = workerExceptionHandler
return this
}
/**
* Specifies a `WorkerExceptionHandler` that can be used to intercept
* exceptions caused when trying to execute [ListenableWorker]s.
*
* This exception handler will be invoked on a thread bound to
* [Configuration.taskExecutor].
*
* @param workerExceptionHandler an instance to handle exceptions
* @return This [Builder] instance
*/
fun setWorkerExecutionExceptionHandler(
workerExceptionHandler: Consumer<WorkerExceptionInfo>
): Builder {
this.workerExecutionExceptionHandler = workerExceptionHandler
return this
}
/**
* Designates the primary process that [WorkManager] should schedule work in.
*
* @param processName The [String] process name.
* @return This [Builder] instance
*/
fun setDefaultProcessName(processName: String): Builder {
defaultProcessName = processName
return this
}
/**
* Builds a [Configuration] object.
*
* @return A [Configuration] object with this [Builder]'s parameters.
*/
fun build(): Configuration {
return Configuration(this)
}
}
/**
* A class that can provide the [Configuration] for WorkManager and allow for on-demand
* initialization of WorkManager. To do this:
*
* - Disable `androidx.work.WorkManagerInitializer` in your manifest
* - Implement the [Configuration.Provider] interface on your [android.app.Application] class
* - Use [WorkManager.getInstance] when accessing WorkManager (NOT [WorkManager.getInstance])
*
* Note that on-demand initialization may delay some useful features of WorkManager such as
* automatic rescheduling of work following a crash and recovery from the application being
* force-stopped by the user or device.
*
* @see WorkManager.initialize
*/
interface Provider {
/**
* The [Configuration] used to initialize WorkManager
*/
val workManagerConfiguration: Configuration
}
companion object {
/**
* The minimum number of system requests which can be enqueued by [WorkManager]
* when using [android.app.job.JobScheduler] or [android.app.AlarmManager].
*/
const val MIN_SCHEDULER_LIMIT = 20
}
}
internal const val DEFAULT_CONTENT_URI_TRIGGERS_WORKERS_LIMIT = 8
private fun createDefaultExecutor(isTaskExecutor: Boolean): Executor {
val factory = object : ThreadFactory {
private val threadCount = AtomicInteger(0)
override fun newThread(runnable: Runnable): Thread {
// Thread names are constrained to a max of 15 characters by the Linux Kernel.
val prefix = if (isTaskExecutor) "WM.task-" else "androidx.work-"
return Thread(runnable, "$prefix${threadCount.incrementAndGet()}")
}
}
return Executors.newFixedThreadPool(
// This value is the same as the core pool size for AsyncTask#THREAD_POOL_EXECUTOR.
max(2, min(Runtime.getRuntime().availableProcessors() - 1, 4)),
factory
)
}
private fun CoroutineContext?.asExecutor(): Executor? =
(this?.get(ContinuationInterceptor) as? CoroutineDispatcher)?.asExecutor()