blob: 9bead9410a510f24cc6ebd7ace925d7068743da3 [file] [log] [blame]
/*
* 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 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;
import kotlinx.coroutines.CoroutineScope;
/**
* 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);
}
}