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/src/com/android/systemui/process/ProcessWrapper.java b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java new file mode 100644 index 0000000000000..7db293d96a503 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.process; + +/** + * A simple wrapper that provides access to process-related details. This facilitates testing by + * providing a mockable target around these details. + */ +public class ProcessWrapper { + public int getUserHandleIdentifier() { + return android.os.Process.myUserHandle().getIdentifier(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/process/condition/UserProcessCondition.java b/packages/SystemUI/src/com/android/systemui/process/condition/UserProcessCondition.java new file mode 100644 index 0000000000000..5a21ea075ea3b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/process/condition/UserProcessCondition.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.process.condition; + +import com.android.systemui.process.ProcessWrapper; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.shared.condition.Condition; + +import javax.inject.Inject; + +/** + * {@link UserProcessCondition} provides a signal when the process handle belongs to the current + * user. + */ +public class UserProcessCondition extends Condition { + private final ProcessWrapper mProcessWrapper; + private final UserTracker mUserTracker; + + @Inject + public UserProcessCondition(ProcessWrapper processWrapper, UserTracker userTracker) { + mProcessWrapper = processWrapper; + mUserTracker = userTracker; + } + + @Override + protected void start() { + updateCondition(mUserTracker.getUserId() + == mProcessWrapper.getUserHandleIdentifier()); + } + + @Override + protected void stop() { + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/ConditionalCoreStartable.java b/packages/SystemUI/src/com/android/systemui/util/condition/ConditionalCoreStartable.java new file mode 100644 index 0000000000000..b41bca0d77e1e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/condition/ConditionalCoreStartable.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.condition; + +import com.android.systemui.CoreStartable; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.shared.condition.Monitor; + +import java.util.Set; + +/** + * {@link ConditionalCoreStartable} is a {@link com.android.systemui.CoreStartable} abstract + * implementation where conditions must be met before routines are executed. + */ +public abstract class ConditionalCoreStartable implements CoreStartable { + private final Monitor mMonitor; + private final Set mConditionSet; + private Monitor.Subscription.Token mStartToken; + private Monitor.Subscription.Token mBootCompletedToken; + + public ConditionalCoreStartable(Monitor monitor) { + this(monitor, null); + } + + public ConditionalCoreStartable(Monitor monitor, Set conditionSet) { + mMonitor = monitor; + mConditionSet = conditionSet; + } + + @Override + public final void start() { + if (mConditionSet == null || mConditionSet.isEmpty()) { + onStart(); + return; + } + + mStartToken = mMonitor.addSubscription( + new Monitor.Subscription.Builder(allConditionsMet -> { + if (allConditionsMet) { + mMonitor.removeSubscription(mStartToken); + mStartToken = null; + onStart(); + } + }).addConditions(mConditionSet) + .build()); + } + + protected abstract void onStart(); + + @Override + public final void onBootCompleted() { + if (mConditionSet == null || mConditionSet.isEmpty()) { + bootCompleted(); + return; + } + + mBootCompletedToken = mMonitor.addSubscription( + new Monitor.Subscription.Builder(allConditionsMet -> { + if (allConditionsMet) { + mMonitor.removeSubscription(mBootCompletedToken); + mBootCompletedToken = null; + bootCompleted(); + } + }).addConditions(mConditionSet) + .build()); + } + + protected void bootCompleted() { + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/process/condition/UserProcessConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/process/condition/UserProcessConditionTest.java new file mode 100644 index 0000000000000..2293fc5770293 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/process/condition/UserProcessConditionTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.process.condition; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.process.ProcessWrapper; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.shared.condition.Monitor; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class UserProcessConditionTest extends SysuiTestCase { + @Mock + UserTracker mUserTracker; + + @Mock + ProcessWrapper mProcessWrapper; + + @Mock + Monitor.Callback mCallback; + + private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + /** + * Verifies condition reports false when tracker reports a different user id than the + * identifier from the process handle. + */ + @Test + public void testConditionFailsWithDifferentIds() { + + final Condition condition = new UserProcessCondition(mProcessWrapper, mUserTracker); + when(mProcessWrapper.getUserHandleIdentifier()).thenReturn(0); + when(mUserTracker.getUserId()).thenReturn(1); + + final Monitor monitor = new Monitor(mExecutor); + + monitor.addSubscription(new Monitor.Subscription.Builder(mCallback) + .addCondition(condition) + .build()); + + mExecutor.runAllReady(); + + verify(mCallback).onConditionsChanged(false); + } + + /** + * Verifies condition reports false when tracker reports a different user id than the + * identifier from the process handle. + */ + @Test + public void testConditionSucceedsWithSameIds() { + + final Condition condition = new UserProcessCondition(mProcessWrapper, mUserTracker); + when(mProcessWrapper.getUserHandleIdentifier()).thenReturn(0); + when(mUserTracker.getUserId()).thenReturn(0); + + final Monitor monitor = new Monitor(mExecutor); + + monitor.addSubscription(new Monitor.Subscription.Builder(mCallback) + .addCondition(condition) + .build()); + + mExecutor.runAllReady(); + + verify(mCallback).onConditionsChanged(true); + } + +} 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)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionalCoreStartableTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionalCoreStartableTest.java new file mode 100644 index 0000000000000..5ef62c1e7e8dd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionalCoreStartableTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.condition; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.CoreStartable; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.shared.condition.Monitor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ConditionalCoreStartableTest extends SysuiTestCase { + public static class FakeConditionalCoreStartable extends ConditionalCoreStartable { + interface Callback { + void onStart(); + void bootCompleted(); + } + + private final Callback mCallback; + + public FakeConditionalCoreStartable(Monitor monitor, Set conditions, + Callback callback) { + super(monitor, conditions); + mCallback = callback; + } + + @Override + protected void onStart() { + mCallback.onStart(); + } + + @Override + protected void bootCompleted() { + mCallback.bootCompleted(); + } + } + + + final Set mConditions = new HashSet<>(); + + @Mock + Condition mCondition; + + @Mock + Monitor mMonitor; + + @Mock + FakeConditionalCoreStartable.Callback mCallback; + + @Mock + Monitor.Subscription.Token mSubscriptionToken; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mConditions.clear(); + } + + /** + * Verifies that {@link ConditionalCoreStartable#onStart()} is predicated on conditions being + * met. + */ + @Test + public void testOnStartCallback() { + final CoreStartable coreStartable = + new FakeConditionalCoreStartable(mMonitor, + new HashSet<>(Arrays.asList(mCondition)), + mCallback); + + when(mMonitor.addSubscription(any())).thenReturn(mSubscriptionToken); + coreStartable.start(); + + final ArgumentCaptor subscriptionCaptor = ArgumentCaptor.forClass( + Monitor.Subscription.class); + verify(mMonitor).addSubscription(subscriptionCaptor.capture()); + + final Monitor.Subscription subscription = subscriptionCaptor.getValue(); + + assertThat(subscription.getConditions()).containsExactly(mCondition); + + verify(mCallback, never()).onStart(); + + subscription.getCallback().onConditionsChanged(true); + + verify(mCallback).onStart(); + verify(mMonitor).removeSubscription(mSubscriptionToken); + } + + + /** + * Verifies that {@link ConditionalCoreStartable#bootCompleted()} ()} is predicated on + * conditions being met. + */ + @Test + public void testBootCompleted() { + final CoreStartable coreStartable = + new FakeConditionalCoreStartable(mMonitor, + new HashSet<>(Arrays.asList(mCondition)), + mCallback); + + when(mMonitor.addSubscription(any())).thenReturn(mSubscriptionToken); + coreStartable.onBootCompleted(); + + final ArgumentCaptor subscriptionCaptor = ArgumentCaptor.forClass( + Monitor.Subscription.class); + verify(mMonitor).addSubscription(subscriptionCaptor.capture()); + + final Monitor.Subscription subscription = subscriptionCaptor.getValue(); + + assertThat(subscription.getConditions()).containsExactly(mCondition); + + verify(mCallback, never()).bootCompleted(); + + subscription.getCallback().onConditionsChanged(true); + + verify(mCallback).bootCompleted(); + verify(mMonitor).removeSubscription(mSubscriptionToken); + } +}