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

Commit ee1b95a0 authored by Lucas Silva's avatar Lucas Silva Committed by Automerger Merge Worker
Browse files

Merge "Add support for lazy conditions" into udc-dev am: f51f8677 am: 27065326

parents 3a16539f 27065326
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