WorkConstraintsTracker.kt

/*
 * Copyright (C) 2017 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.constraints

import androidx.annotation.VisibleForTesting
import androidx.work.Logger
import androidx.work.impl.constraints.controllers.BatteryChargingController
import androidx.work.impl.constraints.controllers.BatteryNotLowController
import androidx.work.impl.constraints.controllers.ConstraintController
import androidx.work.impl.constraints.controllers.NetworkConnectedController
import androidx.work.impl.constraints.controllers.NetworkMeteredController
import androidx.work.impl.constraints.controllers.NetworkNotRoamingController
import androidx.work.impl.constraints.controllers.NetworkUnmeteredController
import androidx.work.impl.constraints.controllers.StorageNotLowController
import androidx.work.impl.constraints.trackers.Trackers
import androidx.work.impl.model.WorkSpec

interface WorkConstraintsTracker {
    /**
     * Replaces the list of tracked [WorkSpec]s to monitor if their constraints are met.
     *
     * @param workSpecs A list of [WorkSpec]s to monitor constraints for
     */
    fun replace(workSpecs: Iterable<WorkSpec>)

    /**
     * Resets and clears all tracked [WorkSpec]s.
     */
    fun reset()
}

/**
 * Tracks [WorkSpec]s and their [androidx.work.Constraints], and notifies an optional
 * [WorkConstraintsCallback] when all of their constraints are met or not met.
 */
class WorkConstraintsTrackerImpl @VisibleForTesting internal constructor(
    private val callback: WorkConstraintsCallback?,
    private val constraintControllers: Array<ConstraintController<*>>,
) : WorkConstraintsTracker, ConstraintController.OnConstraintUpdatedCallback {
    // We need to keep hold a lock here for the cases where there is 1 WCT tracking a list of
    // WorkSpecs. Changes in constraints are notified on the main thread. Enqueues / Cancellations
    // occur on the task executor thread pool. So there is a chance of
    // ConcurrentModificationExceptions.
    private val lock: Any = Any()

    /**
     * @param trackers Constraints trackers
     * @param callback     The callback is only necessary when you need
     * [WorkConstraintsTrackerImpl] to notify you about changes in
     * constraints for the list of [WorkSpec]'s that it is tracking.
     */
    constructor(
        trackers: Trackers,
        callback: WorkConstraintsCallback?
    ) : this(
        callback,
        arrayOf(
            BatteryChargingController(trackers.batteryChargingTracker),
            BatteryNotLowController(trackers.batteryNotLowTracker),
            StorageNotLowController(trackers.storageNotLowTracker),
            NetworkConnectedController(trackers.networkStateTracker),
            NetworkUnmeteredController(trackers.networkStateTracker),
            NetworkNotRoamingController(trackers.networkStateTracker),
            NetworkMeteredController(trackers.networkStateTracker)
        )
    )

    /**
     * Replaces the list of tracked [WorkSpec]s to monitor if their constraints are met.
     *
     * @param workSpecs A list of [WorkSpec]s to monitor constraints for
     */
    override fun replace(workSpecs: Iterable<WorkSpec>) {
        synchronized(lock) {
            for (controller in constraintControllers) {
                controller.callback = null
            }
            for (controller in constraintControllers) {
                controller.replace(workSpecs)
            }
            for (controller in constraintControllers) {
                controller.callback = this
            }
        }
    }

    /**
     * Resets and clears all tracked [WorkSpec]s.
     */
    override fun reset() {
        synchronized(lock) {
            for (controller in constraintControllers) {
                controller.reset()
            }
        }
    }

    /**
     * Returns `true` if all the underlying constraints for a given WorkSpec are met.
     *
     * @param workSpecId The [WorkSpec] id
     * @return `true` if all the underlying constraints for a given [WorkSpec] are
     * met.
     */
    fun areAllConstraintsMet(workSpecId: String): Boolean {
        synchronized(lock) {
            val controller = constraintControllers.firstOrNull {
                it.isWorkSpecConstrained(workSpecId)
            }
            if (controller != null) {
                Logger.get().debug(
                    TAG, "Work $workSpecId constrained by ${controller.javaClass.simpleName}"
                )
            }
            return controller == null
        }
    }

    override fun onConstraintMet(workSpecIds: List<String>) {
        synchronized(lock) {
            val unconstrainedWorkSpecIds = workSpecIds.filter { areAllConstraintsMet(it) }
            unconstrainedWorkSpecIds.forEach {
                Logger.get().debug(TAG, "Constraints met for $it")
            }
            callback?.onAllConstraintsMet(unconstrainedWorkSpecIds)
        }
    }

    override fun onConstraintNotMet(workSpecIds: List<String>) {
        synchronized(lock) { callback?.onAllConstraintsNotMet(workSpecIds) }
    }
}

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