diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java index 95675cef91365..209d5e80e2d2a 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java @@ -38,13 +38,19 @@ import javax.inject.Inject; public class Monitor { private final String mTag = getClass().getSimpleName(); private final Executor mExecutor; + private final Set mPreconditions; private final HashMap> mConditions = new HashMap<>(); private final HashMap mSubscriptions = new HashMap<>(); private static class SubscriptionState { private final Subscription mSubscription; + + // A subscription must maintain a reference to any active nested subscription so that it may + // be later removed when the current subscription becomes invalid. + private Subscription.Token mNestedSubscriptionToken; private Boolean mAllConditionsMet; + private boolean mActive; SubscriptionState(Subscription subscription) { mSubscription = subscription; @@ -54,7 +60,27 @@ public class Monitor { return mSubscription.mConditions; } - public void update() { + /** + * Signals that the {@link Subscription} is now being monitored and will receive updates + * based on its conditions. + */ + private void setActive(boolean active) { + if (mActive == active) { + return; + } + + mActive = active; + + final Callback callback = mSubscription.getCallback(); + + if (callback == null) { + return; + } + + callback.onActiveChanged(active); + } + + public void update(Monitor monitor) { final Boolean result = Evaluator.INSTANCE.evaluate(mSubscription.mConditions, Evaluator.OP_AND); // Consider unknown (null) as true @@ -65,7 +91,50 @@ public class Monitor { } mAllConditionsMet = newAllConditionsMet; - mSubscription.mCallback.onConditionsChanged(mAllConditionsMet); + + final Subscription nestedSubscription = mSubscription.getNestedSubscription(); + + if (nestedSubscription != null) { + if (mAllConditionsMet && mNestedSubscriptionToken == null) { + // When all conditions are met for a subscription with a nested subscription + // that is not currently being monitored, add the nested subscription for + // monitor. + mNestedSubscriptionToken = + monitor.addSubscription(nestedSubscription, null); + } else if (!mAllConditionsMet && mNestedSubscriptionToken != null) { + // When conditions are not met and there is an active nested condition, remove + // the nested condition from monitoring. + removeNestedSubscription(monitor); + } + return; + } + + mSubscription.getCallback().onConditionsChanged(mAllConditionsMet); + } + + /** + * Invoked when the {@link Subscription} has been added to the {@link Monitor}. + */ + public void onAdded() { + setActive(true); + } + + /** + * Invoked when the {@link Subscription} has been removed from the {@link Monitor}, + * allowing cleanup code to run. + */ + public void onRemoved(Monitor monitor) { + setActive(false); + removeNestedSubscription(monitor); + } + + private void removeNestedSubscription(Monitor monitor) { + if (mNestedSubscriptionToken == null) { + return; + } + + monitor.removeSubscription(mNestedSubscriptionToken); + mNestedSubscriptionToken = null; } } @@ -77,9 +146,20 @@ public class Monitor { } }; + /** + * Constructor for injected use-cases. By default, no preconditions are present. + */ @Inject public Monitor(@Main Executor executor) { + this(executor, Collections.emptySet()); + } + + /** + * Main constructor, allowing specifying preconditions. + */ + public Monitor(Executor executor, Set preconditions) { mExecutor = executor; + mPreconditions = preconditions; } private void updateConditionMetState(Condition condition) { @@ -91,7 +171,7 @@ public class Monitor { return; } - subscriptions.stream().forEach(token -> mSubscriptions.get(token).update()); + subscriptions.stream().forEach(token -> mSubscriptions.get(token).update(this)); } /** @@ -101,15 +181,25 @@ public class Monitor { * @return A {@link Subscription.Token} that can be used to remove the subscription. */ public Subscription.Token addSubscription(@NonNull Subscription subscription) { + return addSubscription(subscription, mPreconditions); + } + + private Subscription.Token addSubscription(@NonNull Subscription subscription, + Set preconditions) { + // If preconditions are set on the monitor, set up as a nested condition. + final Subscription normalizedCondition = preconditions != null + ? new Subscription.Builder(subscription).addConditions(preconditions).build() + : subscription; + final Subscription.Token token = new Subscription.Token(); - final SubscriptionState state = new SubscriptionState(subscription); + final SubscriptionState state = new SubscriptionState(normalizedCondition); mExecutor.execute(() -> { if (shouldLog()) Log.d(mTag, "adding subscription"); mSubscriptions.put(token, state); // Add and associate conditions. - subscription.getConditions().stream().forEach(condition -> { + normalizedCondition.getConditions().stream().forEach(condition -> { if (!mConditions.containsKey(condition)) { mConditions.put(condition, new ArraySet<>()); condition.addCallback(mConditionCallback); @@ -118,8 +208,10 @@ public class Monitor { mConditions.get(condition).add(token); }); + state.onAdded(); + // Update subscription state. - state.update(); + state.update(this); }); return token; @@ -139,7 +231,9 @@ public class Monitor { return; } - mSubscriptions.remove(token).getConditions().forEach(condition -> { + final SubscriptionState removedSubscription = mSubscriptions.remove(token); + + removedSubscription.getConditions().forEach(condition -> { if (!mConditions.containsKey(condition)) { Log.e(mTag, "condition not present:" + condition); return; @@ -153,6 +247,8 @@ public class Monitor { mConditions.remove(condition); } }); + + removedSubscription.onRemoved(this); }); } @@ -168,12 +264,19 @@ public class Monitor { private final Set mConditions; private final Callback mCallback; - /** - * - */ - public Subscription(Set conditions, Callback callback) { + // A nested {@link Subscription} is a special callback where the specified condition's + // active state is dependent on the conditions of the parent {@link Subscription} being met. + // Once active, the nested subscription's conditions are registered as normal with the + // monitor and its callback (which could also be a nested condition) is triggered based on + // those conditions. The nested condition will be removed from monitor if the outer + // subscription's conditions ever become invalid. + private final Subscription mNestedSubscription; + + private Subscription(Set conditions, Callback callback, + Subscription nestedSubscription) { this.mConditions = Collections.unmodifiableSet(conditions); this.mCallback = callback; + this.mNestedSubscription = nestedSubscription; } public Set getConditions() { @@ -184,6 +287,10 @@ public class Monitor { return mCallback; } + public Subscription getNestedSubscription() { + return mNestedSubscription; + } + /** * A {@link Token} is an identifier that is associated with a {@link Subscription} which is * registered with a {@link Monitor}. @@ -196,14 +303,26 @@ public class Monitor { */ public static class Builder { private final Callback mCallback; + private final Subscription mNestedSubscription; private final ArraySet mConditions; + private final ArraySet mPreconditions; /** * Default constructor specifying the {@link Callback} for the {@link Subscription}. */ public Builder(Callback callback) { + this(null, callback); + } + + public Builder(Subscription nestedSubscription) { + this(nestedSubscription, null); + } + + private Builder(Subscription nestedSubscription, Callback callback) { + mNestedSubscription = nestedSubscription; mCallback = callback; - mConditions = new ArraySet<>(); + mConditions = new ArraySet(); + mPreconditions = new ArraySet(); } /** @@ -216,12 +335,39 @@ public class Monitor { return this; } + /** + * Adds a set of {@link Condition} to be a precondition for {@link Subscription}. + * + * @return The updated {@link Builder}. + */ + public Builder addPreconditions(Set condition) { + if (condition == null) { + return this; + } + mPreconditions.addAll(condition); + return this; + } + + /** + * Adds a {@link Condition} to be a precondition for {@link Subscription}. + * + * @return The updated {@link Builder}. + */ + public Builder addPrecondition(Condition condition) { + mPreconditions.add(condition); + return this; + } + /** * Adds a set of {@link Condition} to be associated with the {@link Subscription}. * * @return The updated {@link Builder}. */ public Builder addConditions(Set condition) { + if (condition == null) { + return this; + } + mConditions.addAll(condition); return this; } @@ -232,7 +378,11 @@ public class Monitor { * @return The resulting {@link Subscription}. */ public Subscription build() { - return new Subscription(mConditions, mCallback); + final Subscription subscription = + new Subscription(mConditions, mCallback, mNestedSubscription); + return !mPreconditions.isEmpty() + ? new Subscription(mPreconditions, null, subscription) + : subscription; } } } @@ -255,5 +405,13 @@ public class Monitor { * only partial conditions have been fulfilled. */ void onConditionsChanged(boolean allConditionsMet); + + /** + * Called when the active state of the {@link Subscription} changes. + * @param active {@code true} when changes to the conditions will affect the + * {@link Subscription}, {@code false} otherwise. + */ + default void onActiveChanged(boolean active) { + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java index 7693fee0a1c47..9eccbb6303abc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java @@ -471,4 +471,142 @@ public class ConditionMonitorTest extends SysuiTestCase { mExecutor.runAllReady(); verify(callback).onConditionsChanged(true); } + + /** + * Ensures that the result of a condition being true leads to its nested condition being + * activated. + */ + @Test + public void testNestedCondition() { + mCondition1.fakeUpdateCondition(false); + final Monitor.Callback callback = + mock(Monitor.Callback.class); + + mCondition2.fakeUpdateCondition(false); + + // Create a nested condition + mConditionMonitor.addSubscription(new Monitor.Subscription.Builder( + new Monitor.Subscription.Builder(callback) + .addCondition(mCondition2) + .build()) + .addCondition(mCondition1) + .build()); + + mExecutor.runAllReady(); + + // Ensure the nested condition callback is not called at all. + verify(callback, never()).onActiveChanged(anyBoolean()); + verify(callback, never()).onConditionsChanged(anyBoolean()); + + // Update the inner condition to true and ensure that the nested condition is not triggered. + mCondition2.fakeUpdateCondition(true); + verify(callback, never()).onConditionsChanged(anyBoolean()); + mCondition2.fakeUpdateCondition(false); + + // Set outer condition and make sure the inner condition becomes active and reports that + // conditions aren't met + mCondition1.fakeUpdateCondition(true); + mExecutor.runAllReady(); + + verify(callback).onActiveChanged(eq(true)); + verify(callback).onConditionsChanged(eq(false)); + + Mockito.clearInvocations(callback); + + // Update the inner condition and make sure the callback is updated. + mCondition2.fakeUpdateCondition(true); + mExecutor.runAllReady(); + + verify(callback).onConditionsChanged(true); + + Mockito.clearInvocations(callback); + // Invalidate outer condition and make sure callback is informed, but the last state is + // not affected. + mCondition1.fakeUpdateCondition(false); + mExecutor.runAllReady(); + + verify(callback).onActiveChanged(eq(false)); + verify(callback, never()).onConditionsChanged(anyBoolean()); + } + + /** + * Ensures a subscription is predicated on its precondition. + */ + @Test + public void testPrecondition() { + mCondition1.fakeUpdateCondition(false); + final Monitor.Callback callback = + mock(Monitor.Callback.class); + + mCondition2.fakeUpdateCondition(false); + + // Create a nested condition + mConditionMonitor.addSubscription(new Monitor.Subscription.Builder(callback) + .addPrecondition(mCondition1) + .addCondition(mCondition2) + .build()); + + mExecutor.runAllReady(); + + // Ensure the nested condition callback is not called at all. + verify(callback, never()).onActiveChanged(anyBoolean()); + verify(callback, never()).onConditionsChanged(anyBoolean()); + + // Update the condition to true and ensure that the nested condition is not triggered. + mCondition2.fakeUpdateCondition(true); + verify(callback, never()).onConditionsChanged(anyBoolean()); + mCondition2.fakeUpdateCondition(false); + + // Set precondition and make sure the inner condition becomes active and reports that + // conditions aren't met + mCondition1.fakeUpdateCondition(true); + mExecutor.runAllReady(); + + verify(callback).onActiveChanged(eq(true)); + verify(callback).onConditionsChanged(eq(false)); + + Mockito.clearInvocations(callback); + + // Update the condition and make sure the callback is updated. + mCondition2.fakeUpdateCondition(true); + mExecutor.runAllReady(); + + verify(callback).onConditionsChanged(true); + + Mockito.clearInvocations(callback); + // Invalidate precondition and make sure callback is informed, but the last state is + // not affected. + mCondition1.fakeUpdateCondition(false); + mExecutor.runAllReady(); + + verify(callback).onActiveChanged(eq(false)); + verify(callback, never()).onConditionsChanged(anyBoolean()); + } + + /** + * Ensure preconditions are applied to every subscription added to a monitor. + */ + @Test + public void testPreconditionMonitor() { + final Monitor.Callback callback = + mock(Monitor.Callback.class); + + mCondition2.fakeUpdateCondition(true); + final Monitor monitor = new Monitor(mExecutor, new HashSet<>(Arrays.asList(mCondition1))); + + monitor.addSubscription(new Monitor.Subscription.Builder(callback) + .addCondition(mCondition2) + .build()); + + mExecutor.runAllReady(); + + verify(callback, never()).onActiveChanged(anyBoolean()); + verify(callback, never()).onConditionsChanged(anyBoolean()); + + mCondition1.fakeUpdateCondition(true); + mExecutor.runAllReady(); + + verify(callback).onActiveChanged(eq(true)); + verify(callback).onConditionsChanged(eq(true)); + } }