1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.shared.condition; 18 19 import android.util.Log; 20 21 import androidx.annotation.IntDef; 22 import androidx.annotation.NonNull; 23 import androidx.lifecycle.Lifecycle; 24 import androidx.lifecycle.LifecycleEventObserver; 25 import androidx.lifecycle.LifecycleOwner; 26 27 import java.lang.annotation.Retention; 28 import java.lang.annotation.RetentionPolicy; 29 import java.lang.ref.WeakReference; 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 import java.util.Collection; 33 import java.util.Iterator; 34 import java.util.List; 35 36 import kotlinx.coroutines.CoroutineScope; 37 38 /** 39 * Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform 40 * its callbacks. 41 */ 42 public abstract class Condition { 43 private final String mTag = getClass().getSimpleName(); 44 45 private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>(); 46 private final boolean mOverriding; 47 private final CoroutineScope mScope; 48 private Boolean mIsConditionMet; 49 private boolean mStarted = false; 50 51 /** 52 * By default, conditions have an initial value of false and are not overriding. 53 */ Condition(CoroutineScope scope)54 public Condition(CoroutineScope scope) { 55 this(scope, false, false); 56 } 57 58 /** 59 * Constructor for specifying initial state and overriding condition attribute. 60 * 61 * @param initialConditionMet Initial state of the condition. 62 * @param overriding Whether this condition overrides others. 63 */ Condition(CoroutineScope scope, Boolean initialConditionMet, boolean overriding)64 protected Condition(CoroutineScope scope, Boolean initialConditionMet, boolean overriding) { 65 mIsConditionMet = initialConditionMet; 66 mOverriding = overriding; 67 mScope = scope; 68 } 69 70 /** 71 * Starts monitoring the condition. 72 */ start()73 protected abstract void start(); 74 75 /** 76 * Stops monitoring the condition. 77 */ stop()78 protected abstract void stop(); 79 80 /** 81 * Condition should be started as soon as there is an active subscription. 82 */ 83 public static final int START_EAGERLY = 0; 84 /** 85 * Condition should be started lazily only if needed. But once started, it will not be cancelled 86 * unless there are no more active subscriptions. 87 */ 88 public static final int START_LAZILY = 1; 89 /** 90 * Condition should be started lazily only if needed, and can be stopped when not needed. This 91 * should be used for conditions which are expensive to keep running. 92 */ 93 public static final int START_WHEN_NEEDED = 2; 94 95 @Retention(RetentionPolicy.SOURCE) 96 @IntDef({START_EAGERLY, START_LAZILY, START_WHEN_NEEDED}) 97 @interface StartStrategy { 98 } 99 100 @StartStrategy getStartStrategy()101 protected abstract int getStartStrategy(); 102 103 /** 104 * Returns whether the current condition overrides 105 */ isOverridingCondition()106 public boolean isOverridingCondition() { 107 return mOverriding; 108 } 109 110 /** 111 * Registers a callback to receive updates once started. This should be called before 112 * {@link #start()}. Also triggers the callback immediately if already started. 113 */ addCallback(@onNull Callback callback)114 public void addCallback(@NonNull Callback callback) { 115 if (shouldLog()) Log.d(mTag, "adding callback"); 116 mCallbacks.add(new WeakReference<>(callback)); 117 118 if (mStarted) { 119 callback.onConditionChanged(this); 120 return; 121 } 122 123 start(); 124 mStarted = true; 125 } 126 127 /** 128 * Removes the provided callback from further receiving updates. 129 */ removeCallback(@onNull Callback callback)130 public void removeCallback(@NonNull Callback callback) { 131 if (shouldLog()) Log.d(mTag, "removing callback"); 132 final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator(); 133 while (iterator.hasNext()) { 134 final Callback cb = iterator.next().get(); 135 if (cb == null || cb == callback) { 136 iterator.remove(); 137 } 138 } 139 140 if (!mCallbacks.isEmpty() || !mStarted) { 141 return; 142 } 143 144 stop(); 145 mStarted = false; 146 } 147 148 /** 149 * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state 150 * and {@link #removeCallback(Callback)} when not resumed automatically. 151 */ observe(LifecycleOwner owner, Callback listener)152 public Callback observe(LifecycleOwner owner, Callback listener) { 153 return observe(owner.getLifecycle(), listener); 154 } 155 156 /** 157 * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state 158 * and {@link #removeCallback(Condition.Callback)} when not resumed automatically. 159 */ observe(Lifecycle lifecycle, Callback listener)160 public Callback observe(Lifecycle lifecycle, Callback listener) { 161 lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner, event) -> { 162 if (event == Lifecycle.Event.ON_RESUME) { 163 addCallback(listener); 164 } else if (event == Lifecycle.Event.ON_PAUSE) { 165 removeCallback(listener); 166 } 167 }); 168 return listener; 169 } 170 171 /** 172 * Updates the value for whether the condition has been fulfilled, and sends an update if the 173 * value changes and any callback is registered. 174 * 175 * @param isConditionMet True if the condition has been fulfilled. False otherwise. 176 */ updateCondition(boolean isConditionMet)177 protected void updateCondition(boolean isConditionMet) { 178 if (mIsConditionMet != null && mIsConditionMet == isConditionMet) { 179 return; 180 } 181 182 if (shouldLog()) Log.d(mTag, "updating condition to " + isConditionMet); 183 mIsConditionMet = isConditionMet; 184 sendUpdate(); 185 } 186 187 /** 188 * Clears the set condition value. This is purposefully separate from 189 * {@link #updateCondition(boolean)} to avoid confusion around {@code null} values. 190 */ clearCondition()191 protected void clearCondition() { 192 if (mIsConditionMet == null) { 193 return; 194 } 195 196 if (shouldLog()) Log.d(mTag, "clearing condition"); 197 198 mIsConditionMet = null; 199 sendUpdate(); 200 } 201 sendUpdate()202 private void sendUpdate() { 203 final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator(); 204 while (iterator.hasNext()) { 205 final Callback cb = iterator.next().get(); 206 if (cb == null) { 207 iterator.remove(); 208 } else { 209 cb.onConditionChanged(this); 210 } 211 } 212 } 213 214 /** 215 * Returns whether the condition is set. This method should be consulted to understand the 216 * value of {@link #isConditionMet()}. 217 * 218 * @return {@code true} if value is present, {@code false} otherwise. 219 */ isConditionSet()220 public boolean isConditionSet() { 221 return mIsConditionMet != null; 222 } 223 224 /** 225 * Returns whether the condition has been met. Note that this method will return {@code false} 226 * if the condition is not set as well. 227 */ isConditionMet()228 public boolean isConditionMet() { 229 return Boolean.TRUE.equals(mIsConditionMet); 230 } 231 shouldLog()232 protected final boolean shouldLog() { 233 return Log.isLoggable(mTag, Log.DEBUG); 234 } 235 getTag()236 protected final String getTag() { 237 if (isOverridingCondition()) { 238 return mTag + "[OVRD]"; 239 } 240 241 return mTag; 242 } 243 244 /** 245 * Returns the state of the condition. 246 * - "Invalid", condition hasn't been set / not monitored 247 * - "True", condition has been met 248 * - "False", condition has not been met 249 */ getState()250 protected final String getState() { 251 if (!isConditionSet()) { 252 return "Invalid"; 253 } 254 return isConditionMet() ? "True" : "False"; 255 } 256 257 /** 258 * Creates a new condition which will only be true when both this condition and all the provided 259 * conditions are true. 260 */ and(@onNull Collection<Condition> others)261 public Condition and(@NonNull Collection<Condition> others) { 262 final List<Condition> conditions = new ArrayList<>(); 263 conditions.add(this); 264 conditions.addAll(others); 265 return new CombinedCondition(mScope, conditions, Evaluator.OP_AND); 266 } 267 268 /** 269 * Creates a new condition which will only be true when both this condition and the provided 270 * condition is true. 271 */ and(@onNull Condition... others)272 public Condition and(@NonNull Condition... others) { 273 return and(Arrays.asList(others)); 274 } 275 276 /** 277 * Creates a new condition which will only be true when either this condition or any of the 278 * provided conditions are true. 279 */ or(@onNull Collection<Condition> others)280 public Condition or(@NonNull Collection<Condition> others) { 281 final List<Condition> conditions = new ArrayList<>(); 282 conditions.add(this); 283 conditions.addAll(others); 284 return new CombinedCondition(mScope, conditions, Evaluator.OP_OR); 285 } 286 287 /** 288 * Creates a new condition which will only be true when either this condition or the provided 289 * condition is true. 290 */ or(@onNull Condition... others)291 public Condition or(@NonNull Condition... others) { 292 return or(Arrays.asList(others)); 293 } 294 295 /** 296 * Callback that receives updates about whether the condition has been fulfilled. 297 */ 298 public interface Callback { 299 /** 300 * Called when the fulfillment of the condition changes. 301 * 302 * @param condition The condition in question. 303 */ onConditionChanged(Condition condition)304 void onConditionChanged(Condition condition); 305 } 306 } 307