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.ArraySet;
20 import android.util.Log;
21 
22 import androidx.annotation.NonNull;
23 
24 import com.android.systemui.dagger.qualifiers.Main;
25 import com.android.systemui.plugins.log.TableLogBufferBase;
26 
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.Set;
31 import java.util.concurrent.Executor;
32 
33 import javax.inject.Inject;
34 
35 /**
36  * {@link Monitor} allows {@link Subscription}s to a set of conditions and monitors whether all of
37  * them have been fulfilled.
38  * <p>
39  * This class should be used as a singleton, to prevent duplicate monitoring of the same conditions.
40  */
41 public class Monitor {
42     private final String mTag = getClass().getSimpleName();
43     private final Executor mExecutor;
44     private final Set<Condition> mPreconditions;
45     private final TableLogBufferBase mLogBuffer;
46 
47     private final HashMap<Condition, ArraySet<Subscription.Token>> mConditions = new HashMap<>();
48     private final HashMap<Subscription.Token, SubscriptionState> mSubscriptions = new HashMap<>();
49 
50     private static class SubscriptionState {
51         private final Subscription mSubscription;
52 
53         // A subscription must maintain a reference to any active nested subscription so that it may
54         // be later removed when the current subscription becomes invalid.
55         private Subscription.Token mNestedSubscriptionToken;
56         private Boolean mAllConditionsMet;
57         private boolean mActive;
58 
SubscriptionState(Subscription subscription)59         SubscriptionState(Subscription subscription) {
60             mSubscription = subscription;
61         }
62 
getConditions()63         public Set<Condition> getConditions() {
64             return mSubscription.mConditions;
65         }
66 
67         /**
68          * Signals that the {@link Subscription} is now being monitored and will receive updates
69          * based on its conditions.
70          */
setActive(boolean active)71         private void setActive(boolean active) {
72             if (mActive == active) {
73                 return;
74             }
75 
76             mActive = active;
77 
78             final Callback callback = mSubscription.getCallback();
79 
80             if (callback == null) {
81                 return;
82             }
83 
84             callback.onActiveChanged(active);
85         }
86 
update(Monitor monitor)87         public void update(Monitor monitor) {
88             final Boolean result = Evaluator.INSTANCE.evaluate(mSubscription.mConditions,
89                     Evaluator.OP_AND);
90             // Consider unknown (null) as true
91             final boolean newAllConditionsMet = result == null || result;
92 
93             if (mAllConditionsMet != null && newAllConditionsMet == mAllConditionsMet) {
94                 return;
95             }
96 
97             mAllConditionsMet = newAllConditionsMet;
98 
99             final Subscription nestedSubscription = mSubscription.getNestedSubscription();
100 
101             if (nestedSubscription != null) {
102                 if (mAllConditionsMet && mNestedSubscriptionToken == null) {
103                     // When all conditions are met for a subscription with a nested subscription
104                     // that is not currently being monitored, add the nested subscription for
105                     // monitor.
106                     mNestedSubscriptionToken =
107                             monitor.addSubscription(nestedSubscription, null);
108                 } else if (!mAllConditionsMet && mNestedSubscriptionToken != null) {
109                     // When conditions are not met and there is an active nested condition, remove
110                     // the nested condition from monitoring.
111                     removeNestedSubscription(monitor);
112                 }
113                 return;
114             }
115 
116             mSubscription.getCallback().onConditionsChanged(mAllConditionsMet);
117         }
118 
119         /**
120          * Invoked when the {@link Subscription} has been added to the {@link Monitor}.
121          */
onAdded()122         public void onAdded() {
123             setActive(true);
124         }
125 
126         /**
127          * Invoked when the {@link Subscription} has been removed from the {@link Monitor},
128          * allowing cleanup code to run.
129          */
onRemoved(Monitor monitor)130         public void onRemoved(Monitor monitor) {
131             setActive(false);
132             removeNestedSubscription(monitor);
133         }
134 
removeNestedSubscription(Monitor monitor)135         private void removeNestedSubscription(Monitor monitor) {
136             if (mNestedSubscriptionToken == null) {
137                 return;
138             }
139 
140             monitor.removeSubscription(mNestedSubscriptionToken);
141             mNestedSubscriptionToken = null;
142         }
143     }
144 
145     // Callback for when each condition has been updated.
146     private final Condition.Callback mConditionCallback = new Condition.Callback() {
147         @Override
148         public void onConditionChanged(Condition condition) {
149             mExecutor.execute(() -> updateConditionMetState(condition));
150         }
151     };
152 
153     /**
154      * Constructor for injected use-cases. By default, no preconditions are present.
155      */
156     @Inject
Monitor(@ain Executor executor)157     public Monitor(@Main Executor executor) {
158         this(executor, Collections.emptySet());
159     }
160 
161     /**
162      * Main constructor, allowing specifying preconditions.
163      */
Monitor(Executor executor, Set<Condition> preconditions)164     public Monitor(Executor executor, Set<Condition> preconditions) {
165         this(executor, preconditions, null);
166     }
167 
168     /**
169      * Main constructor, allowing specifying preconditions and a log buffer for logging.
170      */
Monitor(Executor executor, Set<Condition> preconditions, TableLogBufferBase logBuffer)171     public Monitor(Executor executor, Set<Condition> preconditions, TableLogBufferBase logBuffer) {
172         mExecutor = executor;
173         mPreconditions = preconditions;
174         mLogBuffer = logBuffer;
175     }
176 
updateConditionMetState(Condition condition)177     private void updateConditionMetState(Condition condition) {
178         if (mLogBuffer != null) {
179             mLogBuffer.logChange(/* prefix= */ "", condition.getTag(), condition.getState());
180         }
181 
182         final ArraySet<Subscription.Token> subscriptions = mConditions.get(condition);
183 
184         // It's possible the condition was removed between the time the callback occurred and
185         // update was executed on the main thread.
186         if (subscriptions == null) {
187             return;
188         }
189 
190         subscriptions.stream().forEach(token -> mSubscriptions.get(token).update(this));
191     }
192 
193     /**
194      * Registers a callback and the set of conditions to trigger it.
195      *
196      * @param subscription A {@link Subscription} detailing the desired conditions and callback.
197      * @return A {@link Subscription.Token} that can be used to remove the subscription.
198      */
addSubscription(@onNull Subscription subscription)199     public Subscription.Token addSubscription(@NonNull Subscription subscription) {
200         return addSubscription(subscription, mPreconditions);
201     }
202 
addSubscription(@onNull Subscription subscription, Set<Condition> preconditions)203     private Subscription.Token addSubscription(@NonNull Subscription subscription,
204             Set<Condition> preconditions) {
205         // If preconditions are set on the monitor, set up as a nested condition.
206         final Subscription normalizedCondition = preconditions != null
207                 ? new Subscription.Builder(subscription).addConditions(preconditions).build()
208                 : subscription;
209 
210         final Subscription.Token token = new Subscription.Token();
211         final SubscriptionState state = new SubscriptionState(normalizedCondition);
212 
213         mExecutor.execute(() -> {
214             if (shouldLog()) Log.d(mTag, "adding subscription");
215             mSubscriptions.put(token, state);
216 
217             // Add and associate conditions.
218             normalizedCondition.getConditions().forEach(condition -> {
219                 if (!mConditions.containsKey(condition)) {
220                     mConditions.put(condition, new ArraySet<>());
221                     condition.addCallback(mConditionCallback);
222                 }
223 
224                 mConditions.get(condition).add(token);
225             });
226 
227             state.onAdded();
228 
229             // Update subscription state.
230             state.update(this);
231 
232         });
233         return token;
234     }
235 
236     /**
237      * Removes a subscription from participating in future callbacks.
238      *
239      * @param token The {@link Subscription.Token} returned when the {@link Subscription} was
240      *              originally added.
241      */
removeSubscription(@onNull Subscription.Token token)242     public void removeSubscription(@NonNull Subscription.Token token) {
243         mExecutor.execute(() -> {
244             if (shouldLog()) Log.d(mTag, "removing subscription");
245             if (!mSubscriptions.containsKey(token)) {
246                 Log.e(mTag, "subscription not present:" + token);
247                 return;
248             }
249 
250             final SubscriptionState removedSubscription = mSubscriptions.remove(token);
251 
252             removedSubscription.getConditions().forEach(condition -> {
253                 if (!mConditions.containsKey(condition)) {
254                     Log.e(mTag, "condition not present:" + condition);
255                     return;
256 
257                 }
258                 final Set<Subscription.Token> conditionSubscriptions = mConditions.get(condition);
259 
260                 conditionSubscriptions.remove(token);
261                 if (conditionSubscriptions.isEmpty()) {
262                     condition.removeCallback(mConditionCallback);
263                     mConditions.remove(condition);
264                 }
265             });
266 
267             removedSubscription.onRemoved(this);
268         });
269     }
270 
shouldLog()271     private boolean shouldLog() {
272         return Log.isLoggable(mTag, Log.DEBUG);
273     }
274 
275     /**
276      * A {@link Subscription} represents a set of conditions and a callback that is informed when
277      * these conditions change.
278      */
279     public static class Subscription {
280         private final Set<Condition> mConditions;
281         private final Callback mCallback;
282 
283         // A nested {@link Subscription} is a special callback where the specified condition's
284         // active state is dependent on the conditions of the parent {@link Subscription} being met.
285         // Once active, the nested subscription's conditions are registered as normal with the
286         // monitor and its callback (which could also be a nested condition) is triggered based on
287         // those conditions. The nested condition will be removed from monitor if the outer
288         // subscription's conditions ever become invalid.
289         private final Subscription mNestedSubscription;
290 
Subscription(Set<Condition> conditions, Callback callback, Subscription nestedSubscription)291         private Subscription(Set<Condition> conditions, Callback callback,
292                 Subscription nestedSubscription) {
293             this.mConditions = Collections.unmodifiableSet(conditions);
294             this.mCallback = callback;
295             this.mNestedSubscription = nestedSubscription;
296         }
297 
getConditions()298         public Set<Condition> getConditions() {
299             return mConditions;
300         }
301 
getCallback()302         public Callback getCallback() {
303             return mCallback;
304         }
305 
getNestedSubscription()306         public Subscription getNestedSubscription() {
307             return mNestedSubscription;
308         }
309 
310         /**
311          * A {@link Token} is an identifier that is associated with a {@link Subscription} which is
312          * registered with a {@link Monitor}.
313          */
314         public static class Token {
315         }
316 
317         /**
318          * {@link Builder} is a helper class for constructing a {@link Subscription}.
319          */
320         public static class Builder {
321             private final Callback mCallback;
322             private final Subscription mNestedSubscription;
323             private final ArraySet<Condition> mConditions;
324 
325             /**
326              * Default constructor specifying the {@link Callback} for the {@link Subscription}.
327              */
Builder(Callback callback)328             public Builder(Callback callback) {
329                 this(null, callback);
330             }
331 
Builder(Subscription nestedSubscription)332             public Builder(Subscription nestedSubscription) {
333                 this(nestedSubscription, null);
334             }
335 
Builder(Subscription nestedSubscription, Callback callback)336             private Builder(Subscription nestedSubscription, Callback callback) {
337                 mNestedSubscription = nestedSubscription;
338                 mCallback = callback;
339                 mConditions = new ArraySet<>();
340             }
341 
342             /**
343              * Adds a {@link Condition} to be associated with the {@link Subscription}.
344              *
345              * @return The updated {@link Builder}.
346              */
addCondition(Condition condition)347             public Builder addCondition(Condition condition) {
348                 mConditions.add(condition);
349                 return this;
350             }
351 
352             /**
353              * Adds a set of {@link Condition} to be associated with the {@link Subscription}.
354              *
355              * @return The updated {@link Builder}.
356              */
addConditions(Set<Condition> condition)357             public Builder addConditions(Set<Condition> condition) {
358                 if (condition == null) {
359                     return this;
360                 }
361 
362                 mConditions.addAll(condition);
363                 return this;
364             }
365 
366             /**
367              * Builds the {@link Subscription}.
368              *
369              * @return The resulting {@link Subscription}.
370              */
build()371             public Subscription build() {
372                 return new Subscription(mConditions, mCallback, mNestedSubscription);
373             }
374         }
375     }
376 
377     /**
378      * Callback that receives updates of whether all conditions have been fulfilled.
379      */
380     public interface Callback {
381         /**
382          * Returns the conditions associated with this callback.
383          */
getConditions()384         default ArrayList<Condition> getConditions() {
385             return new ArrayList<>();
386         }
387 
388         /**
389          * Triggered when the fulfillment of all conditions have been met.
390          *
391          * @param allConditionsMet True if all conditions have been fulfilled. False if none or
392          *                         only partial conditions have been fulfilled.
393          */
onConditionsChanged(boolean allConditionsMet)394         void onConditionsChanged(boolean allConditionsMet);
395 
396         /**
397          * Called when the active state of the {@link Subscription} changes.
398          * @param active {@code true} when changes to the conditions will affect the
399          *               {@link Subscription}, {@code false} otherwise.
400          */
onActiveChanged(boolean active)401         default void onActiveChanged(boolean active) {
402         }
403     }
404 }
405