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

Commit 376ff4e9 authored by Bryce Lee's avatar Bryce Lee Committed by Android (Google) Code Review
Browse files

Merge "Convert Condition.java to Kotlin." into main

parents 04274c02 a0d7fcdd
Loading
Loading
Loading
Loading
+21 −9
Original line number Original line Diff line number Diff line
@@ -19,6 +19,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@RunWith(AndroidJUnit4::class)
class ConditionExtensionsTest : SysuiTestCase() {
class ConditionExtensionsTest : SysuiTestCase() {
    private lateinit var testScope: TestScope
    private lateinit var testScope: TestScope
    private val testCallback =
        Condition.Callback {
            // This is a no-op
        }


    @Before
    @Before
    fun setUp() {
    fun setUp() {
@@ -33,7 +37,7 @@ class ConditionExtensionsTest : SysuiTestCase() {


            assertThat(condition.isConditionSet).isFalse()
            assertThat(condition.isConditionSet).isFalse()


            condition.start()
            condition.testStart()
            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionMet).isTrue()
            assertThat(condition.isConditionMet).isTrue()
        }
        }
@@ -46,7 +50,7 @@ class ConditionExtensionsTest : SysuiTestCase() {


            assertThat(condition.isConditionSet).isFalse()
            assertThat(condition.isConditionSet).isFalse()


            condition.start()
            condition.testStart()
            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionMet).isFalse()
            assertThat(condition.isConditionMet).isFalse()
        }
        }
@@ -56,7 +60,7 @@ class ConditionExtensionsTest : SysuiTestCase() {
        testScope.runTest {
        testScope.runTest {
            val flow = emptyFlow<Boolean>()
            val flow = emptyFlow<Boolean>()
            val condition = flow.toCondition(scope = this, Condition.START_EAGERLY)
            val condition = flow.toCondition(scope = this, Condition.START_EAGERLY)
            condition.start()
            condition.testStop()


            assertThat(condition.isConditionSet).isFalse()
            assertThat(condition.isConditionSet).isFalse()
            assertThat(condition.isConditionMet).isFalse()
            assertThat(condition.isConditionMet).isFalse()
@@ -72,7 +76,7 @@ class ConditionExtensionsTest : SysuiTestCase() {
                    strategy = Condition.START_EAGERLY,
                    strategy = Condition.START_EAGERLY,
                    initialValue = true,
                    initialValue = true,
                )
                )
            condition.start()
            condition.testStart()


            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionMet).isTrue()
            assertThat(condition.isConditionMet).isTrue()
@@ -88,7 +92,7 @@ class ConditionExtensionsTest : SysuiTestCase() {
                    strategy = Condition.START_EAGERLY,
                    strategy = Condition.START_EAGERLY,
                    initialValue = false,
                    initialValue = false,
                )
                )
            condition.start()
            condition.testStart()


            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionMet).isFalse()
            assertThat(condition.isConditionMet).isFalse()
@@ -99,7 +103,7 @@ class ConditionExtensionsTest : SysuiTestCase() {
        testScope.runTest {
        testScope.runTest {
            val flow = MutableStateFlow(false)
            val flow = MutableStateFlow(false)
            val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY)
            val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY)
            condition.start()
            condition.testStart()


            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionSet).isTrue()
            assertThat(condition.isConditionMet).isFalse()
            assertThat(condition.isConditionMet).isFalse()
@@ -110,7 +114,7 @@ class ConditionExtensionsTest : SysuiTestCase() {
            flow.value = false
            flow.value = false
            assertThat(condition.isConditionMet).isFalse()
            assertThat(condition.isConditionMet).isFalse()


            condition.stop()
            condition.testStop()
        }
        }


    @Test
    @Test
@@ -120,10 +124,18 @@ class ConditionExtensionsTest : SysuiTestCase() {
            val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY)
            val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY)
            assertThat(flow.subscriptionCount.value).isEqualTo(0)
            assertThat(flow.subscriptionCount.value).isEqualTo(0)


            condition.start()
            condition.testStart()
            assertThat(flow.subscriptionCount.value).isEqualTo(1)
            assertThat(flow.subscriptionCount.value).isEqualTo(1)


            condition.stop()
            condition.testStop()
            assertThat(flow.subscriptionCount.value).isEqualTo(0)
            assertThat(flow.subscriptionCount.value).isEqualTo(0)
        }
        }

    fun Condition.testStart() {
        addCallback(testCallback)
    }

    fun Condition.testStop() {
        removeCallback(testCallback)
    }
}
}
+1 −1
Original line number Original line Diff line number Diff line
@@ -40,7 +40,7 @@ public class FakeCondition extends Condition {
    }
    }


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


+5 −6
Original line number Original line Diff line number Diff line
@@ -41,7 +41,7 @@ class CombinedCondition
constructor(
constructor(
    private val scope: CoroutineScope,
    private val scope: CoroutineScope,
    private val conditions: Collection<Condition>,
    private val conditions: Collection<Condition>,
    @Evaluator.ConditionOperand private val operand: Int
    @Evaluator.ConditionOperand private val operand: Int,
) : Condition(scope, null, false) {
) : Condition(scope, null, false) {


    private var job: Job? = null
    private var job: Job? = null
@@ -54,7 +54,7 @@ constructor(


                lazilyEvaluate(
                lazilyEvaluate(
                        conditions = groupedConditions.getOrDefault(true, emptyList()),
                        conditions = groupedConditions.getOrDefault(true, emptyList()),
                        filterUnknown = true
                        filterUnknown = true,
                    )
                    )
                    .distinctUntilChanged()
                    .distinctUntilChanged()
                    .flatMapLatest { overriddenValue ->
                    .flatMapLatest { overriddenValue ->
@@ -62,7 +62,7 @@ constructor(
                        if (overriddenValue == null) {
                        if (overriddenValue == null) {
                            lazilyEvaluate(
                            lazilyEvaluate(
                                conditions = groupedConditions.getOrDefault(false, emptyList()),
                                conditions = groupedConditions.getOrDefault(false, emptyList()),
                                filterUnknown = false
                                filterUnknown = false,
                            )
                            )
                        } else {
                        } else {
                            flowOf(overriddenValue)
                            flowOf(overriddenValue)
@@ -188,7 +188,6 @@ constructor(
        return startStrategy
        return startStrategy
    }
    }


    override fun getStartStrategy(): Int {
    override val startStrategy: Int
        return _startStrategy
        get() = _startStrategy
    }
}
}
+0 −306
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2022 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 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 kotlinx.coroutines.CoroutineScope;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/**
 * Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform
 * its callbacks.
 */
public abstract class Condition {
    private final String mTag = getClass().getSimpleName();

    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(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(CoroutineScope scope, Boolean initialConditionMet, boolean overriding) {
        mIsConditionMet = initialConditionMet;
        mOverriding = overriding;
        mScope = scope;
    }

    /**
     * Starts monitoring the condition.
     */
    protected abstract void start();

    /**
     * Stops monitoring the 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
     */
    public boolean isOverridingCondition() {
        return mOverriding;
    }

    /**
     * Registers a callback to receive updates once started. This should be called before
     * {@link #start()}. Also triggers the callback immediately if already started.
     */
    public void addCallback(@NonNull Callback callback) {
        if (shouldLog()) Log.d(mTag, "adding callback");
        mCallbacks.add(new WeakReference<>(callback));

        if (mStarted) {
            callback.onConditionChanged(this);
            return;
        }

        start();
        mStarted = true;
    }

    /**
     * Removes the provided callback from further receiving updates.
     */
    public void removeCallback(@NonNull Callback callback) {
        if (shouldLog()) Log.d(mTag, "removing callback");
        final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
        while (iterator.hasNext()) {
            final Callback cb = iterator.next().get();
            if (cb == null || cb == callback) {
                iterator.remove();
            }
        }

        if (!mCallbacks.isEmpty() || !mStarted) {
            return;
        }

        stop();
        mStarted = false;
    }

    /**
     * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state
     * and {@link #removeCallback(Callback)} when not resumed automatically.
     */
    public Callback observe(LifecycleOwner owner, Callback listener) {
        return observe(owner.getLifecycle(), listener);
    }

    /**
     * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state
     * and {@link #removeCallback(Condition.Callback)} when not resumed automatically.
     */
    public Callback observe(Lifecycle lifecycle, Callback listener) {
        lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner, event) -> {
            if (event == Lifecycle.Event.ON_RESUME) {
                addCallback(listener);
            } else if (event == Lifecycle.Event.ON_PAUSE) {
                removeCallback(listener);
            }
        });
        return listener;
    }

    /**
     * Updates the value for whether the condition has been fulfilled, and sends an update if the
     * value changes and any callback is registered.
     *
     * @param isConditionMet True if the condition has been fulfilled. False otherwise.
     */
    protected void updateCondition(boolean isConditionMet) {
        if (mIsConditionMet != null && mIsConditionMet == isConditionMet) {
            return;
        }

        if (shouldLog()) Log.d(mTag, "updating condition to " + isConditionMet);
        mIsConditionMet = isConditionMet;
        sendUpdate();
    }

    /**
     * Clears the set condition value. This is purposefully separate from
     * {@link #updateCondition(boolean)} to avoid confusion around {@code null} values.
     */
    protected void clearCondition() {
        if (mIsConditionMet == null) {
            return;
        }

        if (shouldLog()) Log.d(mTag, "clearing condition");

        mIsConditionMet = null;
        sendUpdate();
    }

    private void sendUpdate() {
        final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
        while (iterator.hasNext()) {
            final Callback cb = iterator.next().get();
            if (cb == null) {
                iterator.remove();
            } else {
                cb.onConditionChanged(this);
            }
        }
    }

    /**
     * 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() {
        return mIsConditionMet != null;
    }

    /**
     * Returns whether the condition has been met. Note that this method will return {@code false}
     * if the condition is not set as well.
     */
    public boolean isConditionMet() {
        return Boolean.TRUE.equals(mIsConditionMet);
    }

    protected final boolean shouldLog() {
        return Log.isLoggable(mTag, Log.DEBUG);
    }

    protected final String getTag() {
        if (isOverridingCondition()) {
            return mTag + "[OVRD]";
        }

        return mTag;
    }

    /**
     * Returns the state of the condition.
     * - "Invalid", condition hasn't been set / not monitored
     * - "True", condition has been met
     * - "False", condition has not been met
     */
    protected final String getState() {
        if (!isConditionSet()) {
            return "Invalid";
        }
        return isConditionMet() ? "True" : "False";
    }

    /**
     * Creates a new condition which will only be true when both this condition and all the provided
     * conditions are true.
     */
    public Condition and(@NonNull Collection<Condition> others) {
        final List<Condition> conditions = new ArrayList<>();
        conditions.add(this);
        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... others) {
        return and(Arrays.asList(others));
    }

    /**
     * Creates a new condition which will only be true when either this condition or any of the
     * provided conditions are true.
     */
    public Condition or(@NonNull Collection<Condition> others) {
        final List<Condition> conditions = new ArrayList<>();
        conditions.add(this);
        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... others) {
        return or(Arrays.asList(others));
    }

    /**
     * Callback that receives updates about whether the condition has been fulfilled.
     */
    public interface Callback {
        /**
         * Called when the fulfillment of the condition changes.
         *
         * @param condition The condition in question.
         */
        void onConditionChanged(Condition condition);
    }
}
+270 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 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 com.android.systemui.shared.condition

import android.util.Log
import androidx.annotation.IntDef
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import java.lang.ref.WeakReference
import kotlinx.coroutines.CoroutineScope

/**
 * Base class for a condition that needs to be fulfilled in order for [Monitor] to inform its
 * callbacks.
 */
abstract class Condition
/**
 * Constructor for specifying initial state and overriding condition attribute.
 *
 * @param initialConditionMet Initial state of the condition.
 * @param overriding Whether this condition overrides others.
 */
@JvmOverloads
protected constructor(
    private val _scope: CoroutineScope,
    private var _isConditionMet: Boolean? = false,
    /** Returns whether the current condition overrides */
    val isOverridingCondition: Boolean = false,
) {
    private val mTag: String = javaClass.simpleName

    private val _callbacks = mutableListOf<WeakReference<Callback>>()
    private var _started = false

    /** Starts monitoring the condition. */
    protected abstract fun start()

    /** Stops monitoring the condition. */
    protected abstract fun stop()

    @Retention(AnnotationRetention.SOURCE)
    @IntDef(START_EAGERLY, START_LAZILY, START_WHEN_NEEDED)
    annotation class StartStrategy

    @get:StartStrategy abstract val startStrategy: Int

    /**
     * Registers a callback to receive updates once started. This should be called before [.start].
     * Also triggers the callback immediately if already started.
     */
    fun addCallback(callback: Callback) {
        if (shouldLog()) Log.d(mTag, "adding callback")
        _callbacks.add(WeakReference(callback))

        if (_started) {
            callback.onConditionChanged(this)
            return
        }

        start()
        _started = true
    }

    /** Removes the provided callback from further receiving updates. */
    fun removeCallback(callback: Callback) {
        if (shouldLog()) Log.d(mTag, "removing callback")
        val iterator = _callbacks.iterator()
        while (iterator.hasNext()) {
            val cb = iterator.next().get()
            if (cb == null || cb === callback) {
                iterator.remove()
            }
        }

        if (_callbacks.isNotEmpty() || !_started) {
            return
        }

        stop()
        _started = false
    }

    /**
     * Wrapper to [.addCallback] when a lifecycle is in the resumed state and [.removeCallback] when
     * not resumed automatically.
     */
    fun observe(owner: LifecycleOwner, listener: Callback): Callback {
        return observe(owner.lifecycle, listener)
    }

    /**
     * Wrapper to [.addCallback] when a lifecycle is in the resumed state and [.removeCallback] when
     * not resumed automatically.
     */
    fun observe(lifecycle: Lifecycle, listener: Callback): Callback {
        lifecycle.addObserver(
            LifecycleEventObserver { lifecycleOwner: LifecycleOwner?, event: Lifecycle.Event ->
                if (event == Lifecycle.Event.ON_RESUME) {
                    addCallback(listener)
                } else if (event == Lifecycle.Event.ON_PAUSE) {
                    removeCallback(listener)
                }
            }
        )
        return listener
    }

    /**
     * Updates the value for whether the condition has been fulfilled, and sends an update if the
     * value changes and any callback is registered.
     *
     * @param isConditionMet True if the condition has been fulfilled. False otherwise.
     */
    protected fun updateCondition(isConditionMet: Boolean) {
        if (_isConditionMet != null && _isConditionMet == isConditionMet) {
            return
        }

        if (shouldLog()) Log.d(mTag, "updating condition to $isConditionMet")
        _isConditionMet = isConditionMet
        sendUpdate()
    }

    /**
     * Clears the set condition value. This is purposefully separate from [.updateCondition] to
     * avoid confusion around `null` values.
     */
    fun clearCondition() {
        if (_isConditionMet == null) {
            return
        }

        if (shouldLog()) Log.d(mTag, "clearing condition")

        _isConditionMet = null
        sendUpdate()
    }

    private fun sendUpdate() {
        val iterator = _callbacks.iterator()
        while (iterator.hasNext()) {
            val cb = iterator.next().get()
            if (cb == null) {
                iterator.remove()
            } else {
                cb.onConditionChanged(this)
            }
        }
    }

    val isConditionSet: Boolean
        /**
         * Returns whether the condition is set. This method should be consulted to understand the
         * value of [.isConditionMet].
         *
         * @return `true` if value is present, `false` otherwise.
         */
        get() = _isConditionMet != null

    val isConditionMet: Boolean
        /**
         * Returns whether the condition has been met. Note that this method will return `false` if
         * the condition is not set as well.
         */
        get() = true == _isConditionMet

    protected fun shouldLog(): Boolean {
        return Log.isLoggable(mTag, Log.DEBUG)
    }

    val tag: String
        get() {
            if (isOverridingCondition) {
                return "$mTag[OVRD]"
            }

            return mTag
        }

    val state: String
        /**
         * Returns the state of the condition.
         * - "Invalid", condition hasn't been set / not monitored
         * - "True", condition has been met
         * - "False", condition has not been met
         */
        get() {
            if (!isConditionSet) {
                return "Invalid"
            }
            return if (isConditionMet) "True" else "False"
        }

    /**
     * Creates a new condition which will only be true when both this condition and all the provided
     * conditions are true.
     */
    fun and(others: Collection<Condition>): Condition {
        val conditions: List<Condition> = listOf(this, *others.toTypedArray())
        return CombinedCondition(_scope, conditions, Evaluator.OP_AND)
    }

    /**
     * Creates a new condition which will only be true when both this condition and the provided
     * condition is true.
     */
    fun and(vararg others: Condition): Condition {
        return and(listOf(*others))
    }

    /**
     * Creates a new condition which will only be true when either this condition or any of the
     * provided conditions are true.
     */
    fun or(others: Collection<Condition>): Condition {
        val conditions: MutableList<Condition> = ArrayList()
        conditions.add(this)
        conditions.addAll(others)
        return CombinedCondition(_scope, conditions, Evaluator.OP_OR)
    }

    /**
     * Creates a new condition which will only be true when either this condition or the provided
     * condition is true.
     */
    fun or(vararg others: Condition): Condition {
        return or(listOf(*others))
    }

    /** Callback that receives updates about whether the condition has been fulfilled. */
    fun interface Callback {
        /**
         * Called when the fulfillment of the condition changes.
         *
         * @param condition The condition in question.
         */
        fun onConditionChanged(condition: Condition)
    }

    companion object {
        /** Condition should be started as soon as there is an active subscription. */
        const val START_EAGERLY: Int = 0

        /**
         * Condition should be started lazily only if needed. But once started, it will not be
         * cancelled unless there are no more active subscriptions.
         */
        const val START_LAZILY: Int = 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.
         */
        const val START_WHEN_NEEDED: Int = 2
    }
}
Loading