ConstraintTrackingWorker.kt

/*
 * 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.impl.workers

import android.content.Context
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.work.ListenableWorker
import androidx.work.ListenableWorker.Result
import androidx.work.Logger
import androidx.work.WorkerParameters
import androidx.work.impl.WorkManagerImpl
import androidx.work.impl.constraints.WorkConstraintsCallback
import androidx.work.impl.constraints.WorkConstraintsTrackerImpl
import androidx.work.impl.model.WorkSpec
import androidx.work.impl.utils.futures.SettableFuture
import com.google.common.util.concurrent.ListenableFuture

/**
 * Is an implementation of a [androidx.work.Worker] that can delegate to a different
 * [androidx.work.Worker] when the constraints are met.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ConstraintTrackingWorker(
    appContext: Context,
    private val workerParameters: WorkerParameters
) : ListenableWorker(appContext, workerParameters), WorkConstraintsCallback {

    private val lock = Any()

    // Marking this volatile as the delegated workers could switch threads.
    @Volatile
    private var areConstraintsUnmet: Boolean = false
    private val future = SettableFuture.create<Result>()

    /**
     * @return The [androidx.work.Worker] used for delegated work
     * @hide
     */
    @get:VisibleForTesting
    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    var delegate: ListenableWorker? = null
        private set

    override fun startWork(): ListenableFuture<Result> {
        backgroundExecutor.execute { setupAndRunConstraintTrackingWork() }
        return future
    }

    private fun setupAndRunConstraintTrackingWork() {
        if (future.isCancelled) return

        val className = inputData.getString(ARGUMENT_CLASS_NAME)
        val logger = Logger.get()
        if (className.isNullOrEmpty()) {
            logger.error(TAG, "No worker to delegate to.")
            future.setFailed()
            return
        }
        delegate = workerFactory.createWorkerWithDefaultFallback(
            applicationContext, className, workerParameters
        )
        if (delegate == null) {
            logger.debug(TAG, "No worker to delegate to.")
            future.setFailed()
            return
        }

        val workManagerImpl = WorkManagerImpl.getInstance(applicationContext)
        // We need to know what the real constraints are for the delegate.
        val workSpec = workManagerImpl.workDatabase.workSpecDao().getWorkSpec(id.toString())
        if (workSpec == null) {
            future.setFailed()
            return
        }
        val workConstraintsTracker = WorkConstraintsTrackerImpl(workManagerImpl.trackers, this)

        // Start tracking
        workConstraintsTracker.replace(listOf(workSpec))
        if (workConstraintsTracker.areAllConstraintsMet(id.toString())) {
            logger.debug(TAG, "Constraints met for delegate $className")

            // Wrapping the call to mDelegate#doWork() in a try catch, because
            // changes in constraints can cause the worker to throw RuntimeExceptions, and
            // that should cause a retry.
            try {
                val innerFuture = delegate!!.startWork()
                innerFuture.addListener({
                    synchronized(lock) {
                        if (areConstraintsUnmet) {
                            future.setRetry()
                        } else {
                            future.setFuture(innerFuture)
                        }
                    }
                }, backgroundExecutor)
            } catch (exception: Throwable) {
                logger.debug(
                    TAG, "Delegated worker $className threw exception in startWork.", exception
                )
                synchronized(lock) {
                    if (areConstraintsUnmet) {
                        logger.debug(TAG, "Constraints were unmet, Retrying.")
                        future.setRetry()
                    } else {
                        future.setFailed()
                    }
                }
            }
        } else {
            logger.debug(
                TAG, "Constraints not met for delegate $className. Requesting retry."
            )
            future.setRetry()
        }
    }

    override fun onStopped() {
        super.onStopped()
        val delegateInner = delegate
        if (delegateInner != null && !delegateInner.isStopped) {
            // Stop is the method that sets the stopped and cancelled bits and invokes onStopped.
            delegateInner.stop()
        }
    }

    override fun onAllConstraintsMet(workSpecs: List<WorkSpec>) {
        // WorkConstraintTracker notifies on the main thread. So we don't want to trampoline
        // between the background thread and the main thread in this case.
    }

    override fun onAllConstraintsNotMet(workSpecs: List<WorkSpec>) {
        // If at any point, constraints are not met mark it so we can retry the work.
        Logger.get().debug(TAG, "Constraints changed for $workSpecs")
        synchronized(lock) { areConstraintsUnmet = true }
    }
}

private fun SettableFuture<Result>.setFailed() = set(Result.failure())
private fun SettableFuture<Result>.setRetry() = set(Result.retry())

private val TAG = Logger.tagWithPrefix("ConstraintTrkngWrkr")

/**
 * The `className` of the [androidx.work.Worker] to delegate to.
 */
internal const val ARGUMENT_CLASS_NAME =
    "androidx.work.impl.workers.ConstraintTrackingWorker.ARGUMENT_CLASS_NAME"