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