Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e026a6ff authored by Bryce Lee's avatar Bryce Lee
Browse files

Add Preconditions and Nested Subscriptions.

This changelist introduces two concepts: Preconditions and Nested
Subscriptions. Preconditions are allow for a Subscription active
state (following its condition(s) changes) to be predicated on a
set of conditions first being true. Preconditions can be set on
both individual Subscriptions and entire Monitors to apply to all
added Subscriptions. Nested Subscriptions allow for the callback
of one Subscription to be the activation of another Subscription.

Bug: 261420432
Test: atest ConditionMonitorTest
Change-Id: Ifbe498bab144c60a88e5e2e2e570325300c9b953
parent 498f1e90
Loading
Loading
Loading
Loading
+171 −13
Original line number Diff line number Diff line
@@ -38,13 +38,19 @@ import javax.inject.Inject;
public class Monitor {
    private final String mTag = getClass().getSimpleName();
    private final Executor mExecutor;
    private final Set<Condition> mPreconditions;

    private final HashMap<Condition, ArraySet<Subscription.Token>> mConditions = new HashMap<>();
    private final HashMap<Subscription.Token, SubscriptionState> 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<Condition> 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<Condition> 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<Condition> mConditions;
        private final Callback mCallback;

        /**
         *
         */
        public Subscription(Set<Condition> 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<Condition> conditions, Callback callback,
                Subscription nestedSubscription) {
            this.mConditions = Collections.unmodifiableSet(conditions);
            this.mCallback = callback;
            this.mNestedSubscription = nestedSubscription;
        }

        public Set<Condition> 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<Condition> mConditions;
            private final ArraySet<Condition> 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> 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> 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) {
        }
    }
}
+138 −0
Original line number Diff line number Diff line
@@ -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));
    }
}