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

Commit a0d7fcdd authored by Bryce Lee's avatar Bryce Lee
Browse files

Convert Condition.java to Kotlin.

Test: atest ConditionTest
Bug: 394520234
Flag: EXEMPT refactor
Change-Id: I6d15131d2396ff14161bd49642f6cef0901e2e29
parent d1f73a99
Loading
Loading
Loading
Loading
+21 −9
Original line number Diff line number Diff line
@@ -19,6 +19,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ConditionExtensionsTest : SysuiTestCase() {
    private lateinit var testScope: TestScope
    private val testCallback =
        Condition.Callback {
            // This is a no-op
        }

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

            assertThat(condition.isConditionSet).isFalse()

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

            assertThat(condition.isConditionSet).isFalse()

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

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

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

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

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

            condition.stop()
            condition.testStop()
        }

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

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

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

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

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

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

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

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

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

    override fun getStartStrategy(): Int {
        return _startStrategy
    }
    override val startStrategy: Int
        get() = _startStrategy
}
+0 −306
Original line number 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 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