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.work.Logger
import androidx.work.StopReason
import androidx.work.impl.constraints.ConstraintsState.ConstraintsMet
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
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch

sealed class ConstraintsState {
    object ConstraintsMet : ConstraintsState()
    data class ConstraintsNotMet(
        @StopReason
        val reason: Int
    ) : ConstraintsState()
}

fun WorkConstraintsTracker.listen(
    spec: WorkSpec,
    dispatcher: CoroutineDispatcher,
    listener: OnConstraintsStateChangedListener
): Job {
    val job = Job()
    CoroutineScope(dispatcher + job).launch {
        track(spec).collect { listener.onConstraintsStateChanged(spec, it) }
    }
    return job
}

fun interface OnConstraintsStateChangedListener {
    fun onConstraintsStateChanged(workSpec: WorkSpec, state: ConstraintsState)
}

class WorkConstraintsTracker(
    private val controllers: List<ConstraintController<*>>
) {
    /**
     * @param trackers Constraints trackers
     */
    constructor(
        trackers: Trackers,
    ) : this(
        listOf(
            BatteryChargingController(trackers.batteryChargingTracker),
            BatteryNotLowController(trackers.batteryNotLowTracker),
            StorageNotLowController(trackers.storageNotLowTracker),
            NetworkConnectedController(trackers.networkStateTracker),
            NetworkUnmeteredController(trackers.networkStateTracker),
            NetworkNotRoamingController(trackers.networkStateTracker),
            NetworkMeteredController(trackers.networkStateTracker)
        )
    )

    fun track(spec: WorkSpec): Flow<ConstraintsState> {
        val flows = controllers.filter { it.hasConstraint(spec) }.map { it.track() }
        return combine(flows) { states ->
            states.firstOrNull { it != ConstraintsMet } ?: ConstraintsMet
        }.distinctUntilChanged()
    }

    fun areAllConstraintsMet(workSpec: WorkSpec): Boolean {
        val controllers = controllers.filter { it.isConstrained(workSpec) }

        if (controllers.isNotEmpty()) {
            Logger.get().debug(
                TAG, "Work ${workSpec.id} constrained by " +
                    controllers.joinToString { it.javaClass.simpleName }
            )
        }
        return controllers.isEmpty()
    }
}

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