From e026a6ff3f0561ea4f76e69696cbbc8b66753a63 Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Fri, 3 Feb 2023 08:48:32 -0800 Subject: [PATCH 1/3] 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 --- .../systemui/shared/condition/Monitor.java | 184 ++++++++++++++++-- .../condition/ConditionMonitorTest.java | 138 +++++++++++++ 2 files changed, 309 insertions(+), 13 deletions(-) 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)); + } } From 099a3b8d20241bda404936e0555ba272b61d8a6e Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Mon, 6 Feb 2023 09:16:48 -0800 Subject: [PATCH 2/3] ConditionalCoreStartable Introduction. This changelist introduces ConditionalCoreStartable, an abstract implementation of CoreStartable that allows conditions to gate actions from lifecycle events (start/boot completed). Bug: 261420431 Test: atest ConditionalCoreStartableTest Change-Id: Ib4121d375f509ec16e8d2e34cdbee4f92f2ed7d6 --- .../condition/ConditionalCoreStartable.java | 84 ++++++++++ .../ConditionalCoreStartableTest.java | 155 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/util/condition/ConditionalCoreStartable.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionalCoreStartableTest.java 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/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); + } +} From 746c301e558cc307248b035066e349c047910e97 Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Fri, 3 Feb 2023 11:43:11 -0800 Subject: [PATCH 3/3] UserProcessCondition Introduction. This changelist introduces UserProcessCondition, a simple condition that is true only when the process user handle identifier matches the current user. This can be used to limit execution of code to only the process ran by the currently running user. Test: atest UserProcessConditionTest Bug: 261420432 Change-Id: I1efabf11b52d09833dc8a59a94b031bed682cab9 --- .../systemui/process/ProcessWrapper.java | 27 +++++ .../condition/UserProcessCondition.java | 48 ++++++++ .../condition/UserProcessConditionTest.java | 105 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java create mode 100644 packages/SystemUI/src/com/android/systemui/process/condition/UserProcessCondition.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/process/condition/UserProcessConditionTest.java 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/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); + } + +}