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