Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b0e25804 authored by Lucas Silva's avatar Lucas Silva
Browse files

Add support for lazy conditions

Conditions now define a start strategy, which determines how they are
run. START_EAGERLY is how conditions function today, it starts the
condition immediately and doesn't stop it.

START_LAZILY will wait to start the condition until the previous one has
emitted a value which did not short-circuit. For AND operand, this means
the previous condition must have emitted true. For OR operand, it would
be false.

START_WHEN_NEEDED is similar to lazy, but will also cancel the condition
when not needed. This can be used for expensive conditions, such as the
low light condition, since it keeps the lowlight sensor on.

Bug: 278875229
Test: atest CombinedConditionTest
Change-Id: Ie81f74357f1d37c771fa91d097fef85ab11fa604
parent 3a2bda18
Loading
Loading
Loading
Loading
+162 −10
Original line number Diff line number Diff line
@@ -16,27 +16,179 @@

package com.android.systemui.shared.condition

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch

/**
 * A higher order [Condition] which combines multiple conditions with a specified
 * [Evaluator.ConditionOperand].
 * [Evaluator.ConditionOperand]. Conditions are executed lazily as-needed.
 *
 * @param scope The [CoroutineScope] to execute in.
 * @param conditions The list of conditions to evaluate. Since conditions are executed lazily, the
 *   ordering is important here.
 * @param operand The [Evaluator.ConditionOperand] to apply to the conditions.
 */
internal class CombinedCondition
@OptIn(ExperimentalCoroutinesApi::class)
class CombinedCondition
constructor(
    private val scope: CoroutineScope,
    private val conditions: Collection<Condition>,
    @Evaluator.ConditionOperand private val operand: Int
) : Condition(null, false), Condition.Callback {
) : Condition(scope, null, false) {

    private var job: Job? = null
    private val _startStrategy by lazy { calculateStartStrategy() }

    override fun start() {
        onConditionChanged(this)
        conditions.forEach { it.addCallback(this) }
    }
        job =
            scope.launch {
                val groupedConditions = conditions.groupBy { it.isOverridingCondition }

    override fun onConditionChanged(condition: Condition) {
        Evaluator.evaluate(conditions, operand)?.also { value -> updateCondition(value) }
            ?: clearCondition()
                lazilyEvaluate(
                        conditions = groupedConditions.getOrDefault(true, emptyList()),
                        filterUnknown = true
                    )
                    .distinctUntilChanged()
                    .flatMapLatest { overriddenValue ->
                        // If there are overriding conditions with values set, they take precedence.
                        if (overriddenValue == null) {
                            lazilyEvaluate(
                                conditions = groupedConditions.getOrDefault(false, emptyList()),
                                filterUnknown = false
                            )
                        } else {
                            flowOf(overriddenValue)
                        }
                    }
                    .collect { conditionMet ->
                        if (conditionMet == null) {
                            clearCondition()
                        } else {
                            updateCondition(conditionMet)
                        }
                    }
            }
    }

    override fun stop() {
        conditions.forEach { it.removeCallback(this) }
        job?.cancel()
        job = null
    }

    /**
     * Evaluates a list of conditions lazily with support for short-circuiting. Conditions are
     * executed serially in the order provided. At any point if the result can be determined, we
     * short-circuit and return the result without executing all conditions.
     */
    private fun lazilyEvaluate(
        conditions: Collection<Condition>,
        filterUnknown: Boolean,
    ): Flow<Boolean?> = callbackFlow {
        val jobs = MutableList<Job?>(conditions.size) { null }
        val values = MutableList<Boolean?>(conditions.size) { null }
        val flows = conditions.map { it.toFlow() }

        fun cancelAllExcept(indexToSkip: Int) {
            for (index in 0 until jobs.size) {
                if (index == indexToSkip) {
                    continue
                }
                if (
                    indexToSkip == -1 ||
                        conditions.elementAt(index).startStrategy == START_WHEN_NEEDED
                ) {
                    jobs[index]?.cancel()
                    jobs[index] = null
                    values[index] = null
                }
            }
        }

        fun collectFlow(index: Int) {
            // Base case which is triggered once we have collected all the flows. In this case,
            // we never short-circuited and therefore should return the fully evaluated
            // conditions.
            if (flows.isEmpty() || index == -1) {
                val filteredValues =
                    if (filterUnknown) {
                        values.filterNotNull()
                    } else {
                        values
                    }
                trySend(Evaluator.evaluate(filteredValues, operand))
                return
            }
            jobs[index] =
                scope.launch {
                    flows.elementAt(index).collect { value ->
                        values[index] = value
                        if (shouldEarlyReturn(value)) {
                            trySend(value)
                            // The overall result is contingent on this condition, so we don't need
                            // to monitor any other conditions.
                            cancelAllExcept(index)
                        } else {
                            collectFlow(jobs.indexOfFirst { it == null })
                        }
                    }
                }
        }

        // Collect any eager conditions immediately.
        var started = false
        for ((index, condition) in conditions.withIndex()) {
            if (condition.startStrategy == START_EAGERLY) {
                collectFlow(index)
                started = true
            }
        }

        // If no eager conditions started, start the first condition to kick off evaluation.
        if (!started) {
            collectFlow(0)
        }
        awaitClose { cancelAllExcept(-1) }
    }

    private fun shouldEarlyReturn(conditionMet: Boolean?): Boolean {
        return when (operand) {
            Evaluator.OP_AND -> conditionMet == false
            Evaluator.OP_OR -> conditionMet == true
            else -> false
        }
    }

    /**
     * Calculate the start strategy for this condition. This depends on the strategies of the child
     * conditions. If there are any eager conditions, we must also start this condition eagerly. In
     * the absence of eager conditions, we check for lazy conditions. In the absence of either, we
     * make the condition only start when needed.
     */
    private fun calculateStartStrategy(): Int {
        var startStrategy = START_WHEN_NEEDED
        for (condition in conditions) {
            when (condition.startStrategy) {
                START_EAGERLY -> return START_EAGERLY
                START_LAZILY -> {
                    startStrategy = START_LAZILY
                }
                START_WHEN_NEEDED -> {
                    // this is the default, so do nothing
                }
            }
        }
        return startStrategy
    }

    override fun getStartStrategy(): Int {
        return _startStrategy
    }
}
+46 −12
Original line number Diff line number Diff line
@@ -18,11 +18,14 @@ package com.android.systemui.shared.condition;

import android.util.Log;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
@@ -30,6 +33,8 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import kotlinx.coroutines.CoroutineScope;

/**
 * Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform
 * its callbacks.
@@ -39,24 +44,27 @@ public abstract class Condition {

    private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
    private final boolean mOverriding;
    private final CoroutineScope mScope;
    private Boolean mIsConditionMet;
    private boolean mStarted = false;

    /**
     * By default, conditions have an initial value of false and are not overriding.
     */
    public Condition() {
        this(false, false);
    public Condition(CoroutineScope scope) {
        this(scope, false, false);
    }

    /**
     * Constructor for specifying initial state and overriding condition attribute.
     *
     * @param initialConditionMet Initial state of the condition.
     * @param overriding          Whether this condition overrides others.
     */
    protected Condition(Boolean initialConditionMet, boolean overriding) {
    protected Condition(CoroutineScope scope, Boolean initialConditionMet, boolean overriding) {
        mIsConditionMet = initialConditionMet;
        mOverriding = overriding;
        mScope = scope;
    }

    /**
@@ -69,6 +77,29 @@ public abstract class Condition {
     */
    protected abstract void stop();

    /**
     * Condition should be started as soon as there is an active subscription.
     */
    public static final int START_EAGERLY = 0;
    /**
     * Condition should be started lazily only if needed. But once started, it will not be cancelled
     * unless there are no more active subscriptions.
     */
    public static final int START_LAZILY = 1;
    /**
     * Condition should be started lazily only if needed, and can be stopped when not needed. This
     * should be used for conditions which are expensive to keep running.
     */
    public static final int START_WHEN_NEEDED = 2;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({START_EAGERLY, START_LAZILY, START_WHEN_NEEDED})
    @interface StartStrategy {
    }

    @StartStrategy
    protected abstract int getStartStrategy();

    /**
     * Returns whether the current condition overrides
     */
@@ -183,6 +214,7 @@ public abstract class Condition {
    /**
     * Returns whether the condition is set. This method should be consulted to understand the
     * value of {@link #isConditionMet()}.
     *
     * @return {@code true} if value is present, {@code false} otherwise.
     */
    public boolean isConditionSet() {
@@ -210,17 +242,18 @@ public abstract class Condition {
     * conditions are true.
     */
    public Condition and(@NonNull Collection<Condition> others) {
        final List<Condition> conditions = new ArrayList<>(others);
        final List<Condition> conditions = new ArrayList<>();
        conditions.add(this);
        return new CombinedCondition(conditions, Evaluator.OP_AND);
        conditions.addAll(others);
        return new CombinedCondition(mScope, conditions, Evaluator.OP_AND);
    }

    /**
     * Creates a new condition which will only be true when both this condition and the provided
     * condition is true.
     */
    public Condition and(@NonNull Condition other) {
        return new CombinedCondition(Arrays.asList(this, other), Evaluator.OP_AND);
    public Condition and(@NonNull Condition... others) {
        return and(Arrays.asList(others));
    }

    /**
@@ -228,17 +261,18 @@ public abstract class Condition {
     * provided conditions are true.
     */
    public Condition or(@NonNull Collection<Condition> others) {
        final List<Condition> conditions = new ArrayList<>(others);
        final List<Condition> conditions = new ArrayList<>();
        conditions.add(this);
        return new CombinedCondition(conditions, Evaluator.OP_OR);
        conditions.addAll(others);
        return new CombinedCondition(mScope, conditions, Evaluator.OP_OR);
    }

    /**
     * Creates a new condition which will only be true when either this condition or the provided
     * condition is true.
     */
    public Condition or(@NonNull Condition other) {
        return new CombinedCondition(Arrays.asList(this, other), Evaluator.OP_OR);
    public Condition or(@NonNull Condition... others) {
        return or(Arrays.asList(others));
    }

    /**
+30 −2
Original line number Diff line number Diff line
package com.android.systemui.shared.condition

import com.android.systemui.shared.condition.Condition.StartStrategy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch

/** Converts a boolean flow to a [Condition] object which can be used with a [Monitor] */
@JvmOverloads
fun Flow<Boolean>.toCondition(scope: CoroutineScope, initialValue: Boolean? = null): Condition {
    return object : Condition(initialValue, false) {
fun Flow<Boolean>.toCondition(
    scope: CoroutineScope,
    @StartStrategy strategy: Int,
    initialValue: Boolean? = null
): Condition {
    return object : Condition(scope, initialValue, false) {
        var job: Job? = null

        override fun start() {
@@ -19,5 +27,25 @@ fun Flow<Boolean>.toCondition(scope: CoroutineScope, initialValue: Boolean? = nu
            job?.cancel()
            job = null
        }

        override fun getStartStrategy() = strategy
    }
}

/** Converts a [Condition] to a boolean flow */
fun Condition.toFlow(): Flow<Boolean?> {
    return callbackFlow {
            val callback =
                Condition.Callback { condition ->
                    if (condition.isConditionSet) {
                        trySend(condition.isConditionMet)
                    } else {
                        trySend(null)
                    }
                }
            addCallback(callback)
            callback.onConditionChanged(this@toFlow)
            awaitClose { removeCallback(callback) }
        }
        .distinctUntilChanged()
}
+26 −10
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ import android.annotation.IntDef
 * Helper for evaluating a collection of [Condition] objects with a given
 * [Evaluator.ConditionOperand]
 */
internal object Evaluator {
object Evaluator {
    /** Operands for combining multiple conditions together */
    @Retention(AnnotationRetention.SOURCE)
    @IntDef(value = [OP_AND, OP_OR])
@@ -70,15 +70,31 @@ internal object Evaluator {
    fun evaluate(conditions: Collection<Condition>, @ConditionOperand operand: Int): Boolean? {
        if (conditions.isEmpty()) return null
        // If there are overriding conditions with values set, they take precedence.
        val targetConditions =
        val values: Collection<Boolean?> =
            conditions
                .filter { it.isConditionSet && it.isOverridingCondition }
                .ifEmpty { conditions }
                .map { condition ->
                    if (condition.isConditionSet) {
                        condition.isConditionMet
                    } else {
                        null
                    }
                }
        return evaluate(values = values, operand = operand)
    }

    /**
     * Evaluates a set of booleans with a given operand
     *
     * @param operand The operand to use when evaluating.
     * @return Either true or false if the value is known, or null if value is unknown
     */
    internal fun evaluate(values: Collection<Boolean?>, @ConditionOperand operand: Int): Boolean? {
        if (values.isEmpty()) return null
        return when (operand) {
            OP_AND ->
                threeValuedAndOrOr(conditions = targetConditions, returnValueIfAnyMatches = false)
            OP_OR ->
                threeValuedAndOrOr(conditions = targetConditions, returnValueIfAnyMatches = true)
            OP_AND -> threeValuedAndOrOr(values = values, returnValueIfAnyMatches = false)
            OP_OR -> threeValuedAndOrOr(values = values, returnValueIfAnyMatches = true)
            else -> null
        }
    }
@@ -90,16 +106,16 @@ internal object Evaluator {
     *   any value is true.
     */
    private fun threeValuedAndOrOr(
        conditions: Collection<Condition>,
        values: Collection<Boolean?>,
        returnValueIfAnyMatches: Boolean
    ): Boolean? {
        var hasUnknown = false
        for (condition in conditions) {
            if (!condition.isConditionSet) {
        for (value in values) {
            if (value == null) {
                hasUnknown = true
                continue
            }
            if (condition.isConditionMet == returnValueIfAnyMatches) {
            if (value == returnValueIfAnyMatches) {
                return returnValueIfAnyMatches
            }
        }
+10 −0
Original line number Diff line number Diff line
@@ -18,11 +18,14 @@ package com.android.systemui.dreams.conditions;

import com.android.internal.app.AssistUtils;
import com.android.internal.app.IVisualQueryDetectionAttentionListener;
import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.shared.condition.Condition;

import javax.inject.Inject;

import kotlinx.coroutines.CoroutineScope;

/**
 * {@link AssistantAttentionCondition} provides a signal when assistant has the user's attention.
 */
@@ -58,8 +61,10 @@ public class AssistantAttentionCondition extends Condition {

    @Inject
    public AssistantAttentionCondition(
            @Application CoroutineScope scope,
            DreamOverlayStateController dreamOverlayStateController,
            AssistUtils assistUtils) {
        super(scope);
        mDreamOverlayStateController = dreamOverlayStateController;
        mAssistUtils = assistUtils;
    }
@@ -75,6 +80,11 @@ public class AssistantAttentionCondition extends Condition {
        mDreamOverlayStateController.removeCallback(mCallback);
    }

    @Override
    protected int getStartStrategy() {
        return START_EAGERLY;
    }

    private void enableVisualQueryDetection() {
        if (mEnabled) {
            return;
Loading