Merge changes I1efabf11,Ib4121d37,Ifbe498ba into tm-qpr-dev

* changes:
  UserProcessCondition Introduction.
  ConditionalCoreStartable Introduction.
  Add Preconditions and Nested Subscriptions.
This commit is contained in:
Bryce Lee
2023-02-07 22:16:49 +00:00
committed by Android (Google) Code Review
7 changed files with 728 additions and 13 deletions

View File

@@ -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) {
}
}
}

View File

@@ -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();
}
}

View File

@@ -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() {
}
}

View File

@@ -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<Condition> mConditionSet;
private Monitor.Subscription.Token mStartToken;
private Monitor.Subscription.Token mBootCompletedToken;
public ConditionalCoreStartable(Monitor monitor) {
this(monitor, null);
}
public ConditionalCoreStartable(Monitor monitor, Set<Condition> 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() {
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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<Condition> conditions,
Callback callback) {
super(monitor, conditions);
mCallback = callback;
}
@Override
protected void onStart() {
mCallback.onStart();
}
@Override
protected void bootCompleted() {
mCallback.bootCompleted();
}
}
final Set<Condition> 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<Monitor.Subscription> 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<Monitor.Subscription> 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);
}
}