From c960b0b22bb2e14f8180784ce9dd543fba9d3538 Mon Sep 17 00:00:00 2001 From: Darrell Shi Date: Fri, 19 Nov 2021 18:46:39 +0000 Subject: [PATCH 1/4] Introduction to communal conditions monitor. Communal conditions monitor takes a set of conditions, monitors whether all have been fulfilled, and informs its registered listeners. Test: atest CommunalConditionTest CommunalConditionsMonitorTest Bug: 202778351 Change-Id: I5b09ec1d678864141788a84eefe5d6de80bfcb77 --- .../conditions/CommunalCondition.java | 130 +++++++++++++ .../conditions/CommunalConditionsMonitor.java | 154 +++++++++++++++ .../communal/dagger/CommunalModule.java | 16 ++ .../communal/CommunalConditionTest.java | 133 +++++++++++++ .../CommunalConditionsMonitorTest.java | 178 ++++++++++++++++++ .../communal/FakeCommunalCondition.java | 35 ++++ 6 files changed, 646 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalCondition.java create mode 100644 packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalConditionsMonitor.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/communal/CommunalConditionTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/communal/CommunalConditionsMonitorTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/communal/FakeCommunalCondition.java diff --git a/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalCondition.java b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalCondition.java new file mode 100644 index 0000000000000..734ab6323ca05 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalCondition.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 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.communal.conditions; + +import android.util.Log; + +import com.android.systemui.statusbar.policy.CallbackController; + +import org.jetbrains.annotations.NotNull; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Base class for a condition that needs to be fulfilled in order for Communal Mode to display. + */ +public abstract class CommunalCondition implements CallbackController { + private final String mTag = getClass().getSimpleName(); + + private final ArrayList> mCallbacks = new ArrayList<>(); + private boolean mIsConditionMet = false; + private boolean mStarted = false; + + /** + * Starts monitoring the condition. + */ + protected abstract void start(); + + /** + * Stops monitoring the condition. + */ + protected abstract void stop(); + + /** + * Registers a callback to receive updates once started. This should be called before + * {@link #start()}. Also triggers the callback immediately if already started. + */ + @Override + public void addCallback(@NotNull Callback callback) { + if (shouldLog()) Log.d(mTag, "adding callback"); + mCallbacks.add(new WeakReference<>(callback)); + + if (mStarted) { + callback.onConditionChanged(this, mIsConditionMet); + return; + } + + start(); + mStarted = true; + } + + /** + * Removes the provided callback from further receiving updates. + */ + @Override + public void removeCallback(@NotNull Callback callback) { + if (shouldLog()) Log.d(mTag, "removing callback"); + final Iterator> iterator = mCallbacks.iterator(); + while (iterator.hasNext()) { + final Callback cb = iterator.next().get(); + if (cb == null || cb == callback) { + iterator.remove(); + } + } + + if (!mCallbacks.isEmpty() || !mStarted) { + return; + } + + stop(); + mStarted = false; + } + + /** + * Updates the value for whether the condition has been fulfilled, and sends an update if the + * value changes and any callback is registered. + * + * @param isConditionMet True if the condition has been fulfilled. False otherwise. + */ + protected void updateCondition(boolean isConditionMet) { + if (mIsConditionMet == isConditionMet) { + return; + } + + if (shouldLog()) Log.d(mTag, "updating condition to " + isConditionMet); + mIsConditionMet = isConditionMet; + + final Iterator> iterator = mCallbacks.iterator(); + while (iterator.hasNext()) { + final Callback cb = iterator.next().get(); + if (cb == null) { + iterator.remove(); + } else { + cb.onConditionChanged(this, mIsConditionMet); + } + } + } + + private boolean shouldLog() { + return Log.isLoggable(mTag, Log.DEBUG); + } + + /** + * Callback that receives updates about whether the condition has been fulfilled. + */ + public interface Callback { + /** + * Called when the fulfillment of the condition changes. + * + * @param condition The condition in question. + * @param isConditionMet True if the condition has been fulfilled. False otherwise. + */ + void onConditionChanged(CommunalCondition condition, boolean isConditionMet); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalConditionsMonitor.java b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalConditionsMonitor.java new file mode 100644 index 0000000000000..4f772da487687 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalConditionsMonitor.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2021 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.communal.conditions; + +import static com.android.systemui.communal.dagger.CommunalModule.COMMUNAL_CONDITIONS; + +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.policy.CallbackController; + +import org.jetbrains.annotations.NotNull; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * {@link CommunalConditionsMonitor} takes in a set of conditions, monitors whether all of them have + * been fulfilled, and informs any registered listeners. + */ +@SysUISingleton +public class CommunalConditionsMonitor implements + CallbackController { + private final String mTag = getClass().getSimpleName(); + + private final ArrayList> mCallbacks = new ArrayList<>(); + + // Set of all conditions that need to be monitored. + private final Set mConditions; + + // Map of values of each condition. + private final HashMap mConditionsMap = new HashMap<>(); + + // Whether all conditions have been met. + private boolean mAllConditionsMet = false; + + // Whether the monitor has started listening for all the conditions. + private boolean mHaveConditionsStarted = false; + + // Callback for when each condition has been updated. + private final CommunalCondition.Callback mConditionCallback = (condition, isConditionMet) -> { + mConditionsMap.put(condition, isConditionMet); + + final boolean newAllConditionsMet = !mConditionsMap.containsValue(false); + + if (newAllConditionsMet == mAllConditionsMet) { + return; + } + + if (shouldLog()) Log.d(mTag, "all conditions met: " + newAllConditionsMet); + mAllConditionsMet = newAllConditionsMet; + + // Updates all callbacks. + final Iterator> iterator = mCallbacks.iterator(); + while (iterator.hasNext()) { + final Callback callback = iterator.next().get(); + if (callback == null) { + iterator.remove(); + } else { + callback.onConditionsChanged(mAllConditionsMet); + } + } + }; + + @Inject + public CommunalConditionsMonitor( + @Named(COMMUNAL_CONDITIONS) Set communalConditions) { + mConditions = communalConditions; + + // Initializes the conditions map and registers a callback for each condition. + mConditions.forEach((condition -> mConditionsMap.put(condition, false))); + } + + @Override + public void addCallback(@NotNull Callback callback) { + if (shouldLog()) Log.d(mTag, "adding callback"); + mCallbacks.add(new WeakReference<>(callback)); + + // Updates the callback immediately. + callback.onConditionsChanged(mAllConditionsMet); + + if (!mHaveConditionsStarted) { + if (shouldLog()) Log.d(mTag, "starting all conditions"); + mConditions.forEach(condition -> condition.addCallback(mConditionCallback)); + mHaveConditionsStarted = true; + } + } + + @Override + public void removeCallback(@NotNull Callback callback) { + if (shouldLog()) Log.d(mTag, "removing callback"); + final Iterator> iterator = mCallbacks.iterator(); + while (iterator.hasNext()) { + final Callback cb = iterator.next().get(); + if (cb == null || cb == callback) { + iterator.remove(); + } + } + + if (mCallbacks.isEmpty() && mHaveConditionsStarted) { + if (shouldLog()) Log.d(mTag, "stopping all conditions"); + mConditions.forEach(condition -> condition.removeCallback(mConditionCallback)); + + mAllConditionsMet = false; + mHaveConditionsStarted = false; + } + } + + /** + * Force updates each condition to the value provided. + */ + @VisibleForTesting + public void overrideAllConditionsMet(boolean value) { + mConditions.forEach(condition -> condition.updateCondition(value)); + } + + private boolean shouldLog() { + return Log.isLoggable(mTag, Log.DEBUG); + } + + /** + * Callback that receives updates of whether all conditions have been fulfilled. + */ + public interface Callback { + /** + * Triggered when the fulfillment of all conditions have been met. + * + * @param allConditionsMet True if all conditions have been fulfilled. False if none or + * only partial conditions have been fulfilled. + */ + void onConditionsChanged(boolean allConditionsMet); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java index 31dd82d986f80..f739ae57fc9f0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java @@ -20,15 +20,20 @@ import android.content.Context; import android.view.View; import android.widget.FrameLayout; +import com.android.systemui.communal.conditions.CommunalCondition; import com.android.systemui.idle.AmbientLightModeMonitor; import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm; import com.android.systemui.idle.dagger.IdleViewComponent; +import java.util.HashSet; +import java.util.Set; + import javax.inject.Named; import dagger.Binds; import dagger.Module; import dagger.Provides; +import dagger.multibindings.ElementsIntoSet; /** * Dagger Module providing Communal-related functionality. @@ -39,6 +44,7 @@ import dagger.Provides; }) public interface CommunalModule { String IDLE_VIEW = "idle_view"; + String COMMUNAL_CONDITIONS = "communal_conditions"; /** */ @Provides @@ -56,4 +62,14 @@ public interface CommunalModule { @Binds AmbientLightModeMonitor.DebounceAlgorithm ambientLightDebounceAlgorithm( LightSensorEventsDebounceAlgorithm algorithm); + + /** + * Provides a set of conditions that need to be fulfilled in order for Communal Mode to display. + */ + @Provides + @ElementsIntoSet + @Named(COMMUNAL_CONDITIONS) + static Set provideCommunalConditions() { + return new HashSet<>(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalConditionTest.java new file mode 100644 index 0000000000000..9d7ef0f68bd69 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalConditionTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2021 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.communal; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.communal.conditions.CommunalCondition; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class CommunalConditionTest extends SysuiTestCase { + private FakeCommunalCondition mCondition; + + @Before + public void setup() { + mCondition = spy(new FakeCommunalCondition()); + } + + @Test + public void addCallback_addFirstCallback_triggerStart() { + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + verify(mCondition).start(); + } + + @Test + public void addCallback_addMultipleCallbacks_triggerStartOnlyOnce() { + final CommunalCondition.Callback callback1 = mock(CommunalCondition.Callback.class); + final CommunalCondition.Callback callback2 = mock(CommunalCondition.Callback.class); + final CommunalCondition.Callback callback3 = mock(CommunalCondition.Callback.class); + + mCondition.addCallback(callback1); + mCondition.addCallback(callback2); + mCondition.addCallback(callback3); + + verify(mCondition, times(1)).start(); + } + + @Test + public void addCallback_alreadyStarted_triggerUpdate() { + final CommunalCondition.Callback callback1 = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback1); + + mCondition.fakeUpdateCondition(true); + + final CommunalCondition.Callback callback2 = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback2); + verify(callback2).onConditionChanged(mCondition, true); + } + + @Test + public void removeCallback_removeLastCallback_triggerStop() { + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + verify(mCondition, never()).stop(); + + mCondition.removeCallback(callback); + verify(mCondition).stop(); + } + + @Test + public void updateCondition_falseToTrue_reportTrue() { + mCondition.fakeUpdateCondition(false); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + + mCondition.fakeUpdateCondition(true); + verify(callback).onConditionChanged(eq(mCondition), eq(true)); + } + + @Test + public void updateCondition_trueToFalse_reportFalse() { + mCondition.fakeUpdateCondition(true); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + + mCondition.fakeUpdateCondition(false); + verify(callback).onConditionChanged(eq(mCondition), eq(false)); + } + + @Test + public void updateCondition_trueToTrue_reportNothing() { + mCondition.fakeUpdateCondition(true); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + + mCondition.fakeUpdateCondition(true); + verify(callback, never()).onConditionChanged(eq(mCondition), anyBoolean()); + } + + @Test + public void updateCondition_falseToFalse_reportNothing() { + mCondition.fakeUpdateCondition(false); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + + mCondition.fakeUpdateCondition(false); + verify(callback, never()).onConditionChanged(eq(mCondition), anyBoolean()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalConditionsMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalConditionsMonitorTest.java new file mode 100644 index 0000000000000..59ddba1b45977 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalConditionsMonitorTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 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.communal; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.communal.conditions.CommunalCondition; +import com.android.systemui.communal.conditions.CommunalConditionsMonitor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.HashSet; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class CommunalConditionsMonitorTest extends SysuiTestCase { + private FakeCommunalCondition mCondition1; + private FakeCommunalCondition mCondition2; + private FakeCommunalCondition mCondition3; + private HashSet mConditions; + + private CommunalConditionsMonitor mCommunalConditionsMonitor; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mCondition1 = spy(new FakeCommunalCondition()); + mCondition2 = spy(new FakeCommunalCondition()); + mCondition3 = spy(new FakeCommunalCondition()); + mConditions = new HashSet<>(Arrays.asList(mCondition1, mCondition2, mCondition3)); + + mCommunalConditionsMonitor = new CommunalConditionsMonitor(mConditions); + } + + @Test + public void addCallback_addFirstCallback_addCallbackToAllConditions() { + final CommunalConditionsMonitor.Callback callback1 = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback1); + mConditions.forEach(condition -> verify(condition).addCallback(any())); + + final CommunalConditionsMonitor.Callback callback2 = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback2); + mConditions.forEach(condition -> verify(condition, times(1)).addCallback(any())); + } + + @Test + public void addCallback_addFirstCallback_reportWithDefaultValue() { + final CommunalConditionsMonitor.Callback callback = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback); + verify(callback).onConditionsChanged(false); + } + + @Test + public void addCallback_addSecondCallback_reportWithExistingValue() { + final CommunalConditionsMonitor.Callback callback1 = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback1); + + mCommunalConditionsMonitor.overrideAllConditionsMet(true); + + final CommunalConditionsMonitor.Callback callback2 = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback2); + verify(callback2).onConditionsChanged(true); + } + + @Test + public void removeCallback_shouldNoLongerReceiveUpdate() { + final CommunalConditionsMonitor.Callback callback = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback); + clearInvocations(callback); + mCommunalConditionsMonitor.removeCallback(callback); + + mCommunalConditionsMonitor.overrideAllConditionsMet(true); + verify(callback, never()).onConditionsChanged(true); + + mCommunalConditionsMonitor.overrideAllConditionsMet(false); + verify(callback, never()).onConditionsChanged(false); + } + + @Test + public void removeCallback_removeLastCallback_removeCallbackFromAllConditions() { + final CommunalConditionsMonitor.Callback callback1 = + mock(CommunalConditionsMonitor.Callback.class); + final CommunalConditionsMonitor.Callback callback2 = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback1); + mCommunalConditionsMonitor.addCallback(callback2); + + mCommunalConditionsMonitor.removeCallback(callback1); + mConditions.forEach(condition -> verify(condition, never()).removeCallback(any())); + + mCommunalConditionsMonitor.removeCallback(callback2); + mConditions.forEach(condition -> verify(condition).removeCallback(any())); + } + + @Test + public void updateCallbacks_allConditionsMet_reportTrue() { + final CommunalConditionsMonitor.Callback callback = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback); + clearInvocations(callback); + + mCondition1.fakeUpdateCondition(true); + mCondition2.fakeUpdateCondition(true); + mCondition3.fakeUpdateCondition(true); + + verify(callback).onConditionsChanged(true); + } + + @Test + public void updateCallbacks_oneConditionStoppedMeeting_reportFalse() { + final CommunalConditionsMonitor.Callback callback = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback); + + mCondition1.fakeUpdateCondition(true); + mCondition2.fakeUpdateCondition(true); + mCondition3.fakeUpdateCondition(true); + clearInvocations(callback); + + mCondition1.fakeUpdateCondition(false); + verify(callback).onConditionsChanged(false); + } + + @Test + public void updateCallbacks_shouldOnlyUpdateWhenValueChanges() { + final CommunalConditionsMonitor.Callback callback = + mock(CommunalConditionsMonitor.Callback.class); + mCommunalConditionsMonitor.addCallback(callback); + verify(callback).onConditionsChanged(false); + clearInvocations(callback); + + mCondition1.fakeUpdateCondition(true); + verify(callback, never()).onConditionsChanged(anyBoolean()); + + mCondition2.fakeUpdateCondition(true); + verify(callback, never()).onConditionsChanged(anyBoolean()); + + mCondition3.fakeUpdateCondition(true); + verify(callback).onConditionsChanged(true); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/FakeCommunalCondition.java b/packages/SystemUI/tests/src/com/android/systemui/communal/FakeCommunalCondition.java new file mode 100644 index 0000000000000..882effd8f1f25 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/FakeCommunalCondition.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 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.communal; + +import com.android.systemui.communal.conditions.CommunalCondition; + +/** + * Fake implementation of {@link CommunalCondition}, and provides a way for tests to update + * condition fulfillment. + */ +public class FakeCommunalCondition extends CommunalCondition { + @Override + public void start() {} + + @Override + public void stop() {} + + public void fakeUpdateCondition(boolean isConditionMet) { + updateCondition(isConditionMet); + } +} From bcdaf0ce3aed85fd2ee4b1bfe1a55bcf2e3e7776 Mon Sep 17 00:00:00 2001 From: Darrell Shi Date: Tue, 30 Nov 2021 23:52:04 +0000 Subject: [PATCH 2/4] Introduce communal setting condition. - Adds a communal setting condition to the conditions monitor. - Replaces the internal communal setting logic in the source monitor with the conditions monitor. Test: atest CommunalSettingConditionTest CommunalSourceMonitorTest Bug: 202778351 Change-Id: Ibc0f30d4aea505d6396ec8ec695f745eca8b9e95 --- .../communal/CommunalSourceMonitor.java | 73 +++++------ .../conditions/CommunalSettingCondition.java | 66 ++++++++++ .../communal/dagger/CommunalModule.java | 7 +- .../CommunalSettingConditionTest.java | 114 ++++++++++++++++++ .../communal/CommunalSourceMonitorTest.java | 60 ++++----- 5 files changed, 248 insertions(+), 72 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalSettingCondition.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSettingConditionTest.java diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSourceMonitor.java b/packages/SystemUI/src/com/android/systemui/communal/CommunalSourceMonitor.java index 2244532cca946..d3018e310c8fe 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSourceMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSourceMonitor.java @@ -16,17 +16,11 @@ package com.android.systemui.communal; -import android.database.ContentObserver; -import android.os.Handler; -import android.os.UserHandle; -import android.provider.Settings; import android.util.Log; -import androidx.annotation.MainThread; - import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.communal.conditions.CommunalConditionsMonitor; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.util.settings.SecureSettings; import com.google.android.collect.Lists; @@ -46,10 +40,15 @@ public class CommunalSourceMonitor { // A list of {@link Callback} that have registered to receive updates. private final ArrayList> mCallbacks = Lists.newArrayList(); - private final SecureSettings mSecureSettings; + private final CommunalConditionsMonitor mConditionsMonitor; private CommunalSource mCurrentSource; - private boolean mCommunalEnabled; + + // Whether all conditions for communal mode to show have been met. + private boolean mAllCommunalConditionsMet = false; + + // Whether the class is currently listening for condition changes. + private boolean mListeningForConditions = false; private CommunalSource.Callback mSourceCallback = new CommunalSource.Callback() { @Override @@ -59,24 +58,20 @@ public class CommunalSourceMonitor { } }; + private final CommunalConditionsMonitor.Callback mConditionsCallback = + allConditionsMet -> { + if (mAllCommunalConditionsMet != allConditionsMet) { + if (DEBUG) Log.d(TAG, "communal conditions changed: " + allConditionsMet); + + mAllCommunalConditionsMet = allConditionsMet; + executeOnSourceAvailableCallbacks(); + } + }; + @VisibleForTesting @Inject - public CommunalSourceMonitor( - @MainThread Handler mainThreadHandler, - SecureSettings secureSettings) { - mSecureSettings = secureSettings; - - ContentObserver settingsObserver = new ContentObserver(mainThreadHandler) { - @Override - public void onChange(boolean selfChange) { - reloadSettings(); - } - }; - mSecureSettings.registerContentObserverForUser( - Settings.Secure.COMMUNAL_MODE_ENABLED, - /* notifyForDescendants= */false, - settingsObserver, UserHandle.USER_SYSTEM); - reloadSettings(); + public CommunalSourceMonitor(CommunalConditionsMonitor communalConditionsMonitor) { + mConditionsMonitor = communalConditionsMonitor; } /** @@ -92,7 +87,7 @@ public class CommunalSourceMonitor { mCurrentSource = source; - if (mCommunalEnabled) { + if (mAllCommunalConditionsMet) { executeOnSourceAvailableCallbacks(); } @@ -111,7 +106,7 @@ public class CommunalSourceMonitor { itr.remove(); } else { cb.onSourceAvailable( - (mCommunalEnabled && mCurrentSource != null) ? new WeakReference<>( + (mAllCommunalConditionsMet && mCurrentSource != null) ? new WeakReference<>( mCurrentSource) : null); } } @@ -126,9 +121,14 @@ public class CommunalSourceMonitor { mCallbacks.add(new WeakReference<>(callback)); // Inform the callback of any already present CommunalSource. - if (mCommunalEnabled && mCurrentSource != null) { + if (mAllCommunalConditionsMet && mCurrentSource != null) { callback.onSourceAvailable(new WeakReference<>(mCurrentSource)); } + + if (!mListeningForConditions) { + mConditionsMonitor.addCallback(mConditionsCallback); + mListeningForConditions = true; + } } /** @@ -138,21 +138,10 @@ public class CommunalSourceMonitor { */ public void removeCallback(Callback callback) { mCallbacks.removeIf(el -> el.get() == callback); - } - private void reloadSettings() { - boolean newCommunalEnabled = mSecureSettings.getIntForUser( - Settings.Secure.COMMUNAL_MODE_ENABLED, - 1, - UserHandle.USER_SYSTEM) == 1; - - if (DEBUG) { - Log.d(TAG, "communal mode settings reloaded with value:" + newCommunalEnabled); - } - - if (mCommunalEnabled != newCommunalEnabled) { - mCommunalEnabled = newCommunalEnabled; - executeOnSourceAvailableCallbacks(); + if (mCallbacks.isEmpty() && mListeningForConditions) { + mConditionsMonitor.removeCallback(mConditionsCallback); + mListeningForConditions = false; } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalSettingCondition.java b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalSettingCondition.java new file mode 100644 index 0000000000000..1616b18172a9d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalSettingCondition.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 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.communal.conditions; + +import android.database.ContentObserver; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.annotation.MainThread; + +import com.android.systemui.util.settings.SecureSettings; + +import javax.inject.Inject; + +/** + * Monitors the communal setting, and informs any listeners with updates. + */ +public class CommunalSettingCondition extends CommunalCondition { + private final SecureSettings mSecureSettings; + private final ContentObserver mCommunalSettingContentObserver; + + @Inject + public CommunalSettingCondition(@MainThread Handler mainHandler, + SecureSettings secureSettings) { + mSecureSettings = secureSettings; + + mCommunalSettingContentObserver = new ContentObserver(mainHandler) { + @Override + public void onChange(boolean selfChange) { + final boolean communalSettingEnabled = mSecureSettings.getIntForUser( + Settings.Secure.COMMUNAL_MODE_ENABLED, 0, UserHandle.USER_SYSTEM) == 1; + updateCondition(communalSettingEnabled); + } + }; + } + + @Override + protected void start() { + mSecureSettings.registerContentObserverForUser(Settings.Secure.COMMUNAL_MODE_ENABLED, + false /*notifyForDescendants*/, mCommunalSettingContentObserver, + UserHandle.USER_SYSTEM); + + // Fetches setting immediately. + mCommunalSettingContentObserver.onChange(false); + } + + @Override + protected void stop() { + mSecureSettings.unregisterContentObserver(mCommunalSettingContentObserver); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java index f739ae57fc9f0..9b72655c9fb5b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java @@ -21,10 +21,12 @@ import android.view.View; import android.widget.FrameLayout; import com.android.systemui.communal.conditions.CommunalCondition; +import com.android.systemui.communal.conditions.CommunalSettingCondition; import com.android.systemui.idle.AmbientLightModeMonitor; import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm; import com.android.systemui.idle.dagger.IdleViewComponent; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -69,7 +71,8 @@ public interface CommunalModule { @Provides @ElementsIntoSet @Named(COMMUNAL_CONDITIONS) - static Set provideCommunalConditions() { - return new HashSet<>(); + static Set provideCommunalConditions( + CommunalSettingCondition communalSettingCondition) { + return new HashSet<>(Collections.singletonList(communalSettingCondition)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSettingConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSettingConditionTest.java new file mode 100644 index 0000000000000..cf147f06979fa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSettingConditionTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 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.communal; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.communal.conditions.CommunalCondition; +import com.android.systemui.communal.conditions.CommunalSettingCondition; +import com.android.systemui.util.settings.FakeSettings; +import com.android.systemui.utils.os.FakeHandler; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class CommunalSettingConditionTest extends SysuiTestCase { + private FakeSettings mSecureSettings; + private CommunalSettingCondition mCondition; + + @Before + public void setup() { + final FakeHandler handler = new FakeHandler(Looper.getMainLooper()); + mSecureSettings = new FakeSettings(); + mCondition = new CommunalSettingCondition(handler, mSecureSettings); + } + + @Test + public void addCallback_communalSettingEnabled_immediatelyReportsTrue() { + updateCommunalSetting(true); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + verify(callback).onConditionChanged(mCondition, true); + } + + @Test + public void addCallback_communalSettingDisabled_noReport() { + updateCommunalSetting(false); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + verify(callback, never()).onConditionChanged(eq(mCondition), anyBoolean()); + } + + @Test + public void updateCallback_communalSettingEnabled_reportsTrue() { + updateCommunalSetting(false); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + clearInvocations(callback); + + updateCommunalSetting(true); + verify(callback).onConditionChanged(mCondition, true); + } + + @Test + public void updateCallback_communalSettingDisabled_reportsFalse() { + updateCommunalSetting(true); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + clearInvocations(callback); + + updateCommunalSetting(false); + verify(callback).onConditionChanged(mCondition, false); + } + + @Test + public void updateCallback_communalSettingDidNotChange_neverReportDup() { + updateCommunalSetting(true); + + final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class); + mCondition.addCallback(callback); + clearInvocations(callback); + + updateCommunalSetting(true); + verify(callback, never()).onConditionChanged(mCondition, true); + } + + private void updateCommunalSetting(boolean value) { + mSecureSettings.putIntForUser(Settings.Secure.COMMUNAL_MODE_ENABLED, value ? 1 : 0, + UserHandle.USER_SYSTEM); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSourceMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSourceMonitorTest.java index 68600de81085c..df9a2cb1d268a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSourceMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSourceMonitorTest.java @@ -20,25 +20,25 @@ package com.android.systemui.communal; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import android.os.Handler; -import android.os.UserHandle; -import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.util.settings.FakeSettings; +import com.android.systemui.communal.conditions.CommunalConditionsMonitor; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.lang.ref.WeakReference; @@ -47,15 +47,16 @@ import java.lang.ref.WeakReference; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class CommunalSourceMonitorTest extends SysuiTestCase { + @Mock private CommunalConditionsMonitor mCommunalConditionsMonitor; + + @Captor private ArgumentCaptor mConditionsCallbackCaptor; + private CommunalSourceMonitor mCommunalSourceMonitor; - private FakeSettings mSecureSettings; @Before public void setup() { MockitoAnnotations.initMocks(this); - mSecureSettings = new FakeSettings(); - mCommunalSourceMonitor = new CommunalSourceMonitor( - Handler.createAsync(TestableLooper.get(this).getLooper()), mSecureSettings); + mCommunalSourceMonitor = new CommunalSourceMonitor(mCommunalConditionsMonitor); } @Test @@ -65,6 +66,7 @@ public class CommunalSourceMonitorTest extends SysuiTestCase { mCommunalSourceMonitor.setSource(source); mCommunalSourceMonitor.addCallback(callback); + setConditionsMet(true); verifyOnSourceAvailableCalledWith(callback, source); } @@ -82,33 +84,32 @@ public class CommunalSourceMonitorTest extends SysuiTestCase { } @Test - public void testAddCallbackWithDefaultSetting() { + public void testAddCallbackWithConditionsMet() { final CommunalSourceMonitor.Callback callback = mock(CommunalSourceMonitor.Callback.class); final CommunalSource source = mock(CommunalSource.class); mCommunalSourceMonitor.addCallback(callback); + setConditionsMet(true); + clearInvocations(callback); mCommunalSourceMonitor.setSource(source); verifyOnSourceAvailableCalledWith(callback, source); } @Test - public void testAddCallbackWithSettingDisabled() { - setCommunalEnabled(false); - + public void testAddCallbackWithConditionsNotMet() { final CommunalSourceMonitor.Callback callback = mock(CommunalSourceMonitor.Callback.class); final CommunalSource source = mock(CommunalSource.class); mCommunalSourceMonitor.addCallback(callback); + setConditionsMet(false); mCommunalSourceMonitor.setSource(source); verify(callback, never()).onSourceAvailable(any()); } @Test - public void testSettingEnabledAfterCallbackAdded() { - setCommunalEnabled(false); - + public void testConditionsAreMetAfterCallbackAdded() { final CommunalSourceMonitor.Callback callback = mock(CommunalSourceMonitor.Callback.class); final CommunalSource source = mock(CommunalSource.class); @@ -118,33 +119,27 @@ public class CommunalSourceMonitorTest extends SysuiTestCase { // The callback should not have executed since communal is disabled. verify(callback, never()).onSourceAvailable(any()); - // The callback should execute when the user changes the setting to enabled. - setCommunalEnabled(true); + // The callback should execute when all conditions are met. + setConditionsMet(true); verifyOnSourceAvailableCalledWith(callback, source); } @Test - public void testSettingDisabledAfterCallbackAdded() { + public void testConditionsNoLongerMetAfterCallbackAdded() { final CommunalSourceMonitor.Callback callback = mock(CommunalSourceMonitor.Callback.class); final CommunalSource source = mock(CommunalSource.class); mCommunalSourceMonitor.addCallback(callback); mCommunalSourceMonitor.setSource(source); + setConditionsMet(true); verifyOnSourceAvailableCalledWith(callback, source); - // The callback should execute again when the setting is disabled, with a value of null. - setCommunalEnabled(false); + // The callback should execute again when conditions are no longer met, with a value of + // null. + setConditionsMet(false); verify(callback).onSourceAvailable(null); } - private void setCommunalEnabled(boolean enabled) { - mSecureSettings.putIntForUser( - Settings.Secure.COMMUNAL_MODE_ENABLED, - enabled ? 1 : 0, - UserHandle.USER_SYSTEM); - TestableLooper.get(this).processAllMessages(); - } - private void verifyOnSourceAvailableCalledWith(CommunalSourceMonitor.Callback callback, CommunalSource source) { final ArgumentCaptor> sourceCapture = @@ -152,4 +147,13 @@ public class CommunalSourceMonitorTest extends SysuiTestCase { verify(callback).onSourceAvailable(sourceCapture.capture()); assertThat(sourceCapture.getValue().get()).isEqualTo(source); } + + // Pushes an update on whether the communal conditions are met, assuming that a callback has + // been registered with the communal conditions monitor. + private void setConditionsMet(boolean value) { + verify(mCommunalConditionsMonitor).addCallback(mConditionsCallbackCaptor.capture()); + final CommunalConditionsMonitor.Callback conditionsCallback = + mConditionsCallbackCaptor.getValue(); + conditionsCallback.onConditionsChanged(value); + } } From e4f57c4994d43ab9f3c10cc8dfa650e8e29e0859 Mon Sep 17 00:00:00 2001 From: Darrell Shi Date: Wed, 1 Dec 2021 00:23:53 +0000 Subject: [PATCH 3/4] Introduce communal trusted network condition. This condition, when started, monitors Wi-Fi connections and reports whether the device is currently connected to a trusted network. Test: atest CommunalTrustedNetworkConditionTest Bug: 202778351 Change-Id: Idcc93f9eb73075129ccba9b02b9ccb416ee07aaa --- .../CommunalTrustedNetworkCondition.java | 122 ++++++++++++++++++ .../communal/dagger/CommunalModule.java | 9 +- .../CommunalTrustedNetworkConditionTest.java | 112 ++++++++++++++++ 3 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalTrustedNetworkCondition.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/communal/CommunalTrustedNetworkConditionTest.java diff --git a/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalTrustedNetworkCondition.java b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalTrustedNetworkCondition.java new file mode 100644 index 0000000000000..c817979ee3c5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalTrustedNetworkCondition.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2021 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.communal.conditions; + +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.wifi.WifiInfo; +import android.util.Log; + +import androidx.annotation.NonNull; + +import javax.inject.Inject; + +/** + * Monitors Wi-Fi connections and triggers callback, if any, when the device is connected to and + * disconnected from a trusted network. + */ +public class CommunalTrustedNetworkCondition extends CommunalCondition { + private final String mTag = getClass().getSimpleName(); + private final ConnectivityManager mConnectivityManager; + + @Inject + public CommunalTrustedNetworkCondition(ConnectivityManager connectivityManager) { + mConnectivityManager = connectivityManager; + } + + /** + * Starts monitoring for trusted network connection. Ignores if already started. + */ + @Override + protected void start() { + if (shouldLog()) Log.d(mTag, "start listening for wifi connections"); + + final NetworkRequest wifiNetworkRequest = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + mConnectivityManager.registerNetworkCallback(wifiNetworkRequest, mNetworkCallback); + } + + /** + * Stops monitoring for trusted network connection. + */ + @Override + protected void stop() { + if (shouldLog()) Log.d(mTag, "stop listening for wifi connections"); + + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + } + + private void updateWifiInfo(WifiInfo wifiInfo) { + // TODO(b/202778351): check whether wifi is trusted. + final boolean connectedToTrustedNetwork = wifiInfo != null; + + if (shouldLog()) { + Log.d(mTag, (connectedToTrustedNetwork ? "connected to" : "disconnected from") + + " a trusted network"); + } + + updateCondition(connectedToTrustedNetwork); + } + + private final ConnectivityManager.NetworkCallback mNetworkCallback = + new ConnectivityManager.NetworkCallback( + ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) { + private boolean mIsConnected = false; + private WifiInfo mWifiInfo; + + @Override + public void onAvailable(@NonNull Network network) { + super.onAvailable(network); + + if (shouldLog()) Log.d(mTag, "connected to wifi"); + + mIsConnected = true; + if (mWifiInfo != null) { + updateWifiInfo(mWifiInfo); + } + } + + @Override + public void onLost(@NonNull Network network) { + super.onLost(network); + + if (shouldLog()) Log.d(mTag, "disconnected from wifi"); + + mIsConnected = false; + mWifiInfo = null; + updateWifiInfo(null); + } + + @Override + public void onCapabilitiesChanged(@NonNull Network network, + @NonNull NetworkCapabilities networkCapabilities) { + super.onCapabilitiesChanged(network, networkCapabilities); + + mWifiInfo = (WifiInfo) networkCapabilities.getTransportInfo(); + + if (mIsConnected) { + updateWifiInfo(mWifiInfo); + } + } + }; + + private boolean shouldLog() { + return Log.isLoggable(mTag, Log.DEBUG); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java index 9b72655c9fb5b..3ebfb513dfc5a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java @@ -22,11 +22,12 @@ import android.widget.FrameLayout; import com.android.systemui.communal.conditions.CommunalCondition; import com.android.systemui.communal.conditions.CommunalSettingCondition; +import com.android.systemui.communal.conditions.CommunalTrustedNetworkCondition; import com.android.systemui.idle.AmbientLightModeMonitor; import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm; import com.android.systemui.idle.dagger.IdleViewComponent; -import java.util.Collections; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -72,7 +73,9 @@ public interface CommunalModule { @ElementsIntoSet @Named(COMMUNAL_CONDITIONS) static Set provideCommunalConditions( - CommunalSettingCondition communalSettingCondition) { - return new HashSet<>(Collections.singletonList(communalSettingCondition)); + CommunalSettingCondition communalSettingCondition, + CommunalTrustedNetworkCondition communalTrustedNetworkCondition) { + return new HashSet<>( + Arrays.asList(communalSettingCondition, communalTrustedNetworkCondition)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalTrustedNetworkConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalTrustedNetworkConditionTest.java new file mode 100644 index 0000000000000..a00c12b113a17 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalTrustedNetworkConditionTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2021 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.communal; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.wifi.WifiInfo; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.communal.conditions.CommunalTrustedNetworkCondition; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class CommunalTrustedNetworkConditionTest extends SysuiTestCase { + @Mock private ConnectivityManager mConnectivityManager; + + @Captor private ArgumentCaptor mNetworkCallbackCaptor; + + private CommunalTrustedNetworkCondition mCondition; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mCondition = new CommunalTrustedNetworkCondition(mConnectivityManager); + } + + @Test + public void updateCallback_connectedToTrustedNetwork_reportsTrue() { + final CommunalTrustedNetworkCondition.Callback callback = + mock(CommunalTrustedNetworkCondition.Callback.class); + mCondition.addCallback(callback); + + final ConnectivityManager.NetworkCallback networkCallback = captureNetworkCallback(); + + // Connected to Wi-Fi. + final Network network = mock(Network.class); + networkCallback.onAvailable(network); + networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities()); + + // Verifies that the callback is triggered. + verify(callback).onConditionChanged(mCondition, true); + } + + @Test + public void updateCallback_disconnectedFromTrustedNetwork_reportsFalse() { + final CommunalTrustedNetworkCondition.Callback callback = + mock(CommunalTrustedNetworkCondition.Callback.class); + mCondition.addCallback(callback); + + final ConnectivityManager.NetworkCallback networkCallback = captureNetworkCallback(); + + // Connected to Wi-Fi. + final Network network = mock(Network.class); + networkCallback.onAvailable(network); + networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities()); + clearInvocations(callback); + + // Disconnected from Wi-Fi. + networkCallback.onLost(network); + + // Verifies that the callback is triggered. + verify(callback).onConditionChanged(mCondition, false); + } + + // Captures and returns the network callback, assuming it is registered with the connectivity + // manager. + private ConnectivityManager.NetworkCallback captureNetworkCallback() { + verify(mConnectivityManager).registerNetworkCallback(any(NetworkRequest.class), + mNetworkCallbackCaptor.capture()); + return mNetworkCallbackCaptor.getValue(); + } + + private NetworkCapabilities fakeNetworkCapabilities() { + final NetworkCapabilities networkCapabilities = mock(NetworkCapabilities.class); + final WifiInfo wifiInfo = mock(WifiInfo.class); + when(networkCapabilities.getTransportInfo()).thenReturn(wifiInfo); + return networkCapabilities; + } +} From d193321034024733f8d8ad4b32520fb6af68d99d Mon Sep 17 00:00:00 2001 From: Darrell Shi Date: Wed, 24 Nov 2021 18:16:54 +0000 Subject: [PATCH 4/4] Check whether a Wi-Fi network is trusted. When a Wi-Fi network is connected, check if it's in the trusted listed set by settings. Only when the network is trusted should the condition be fulfilled. Test: atest CommunalTrustedNetworkConditionTest Bug: 202778351 Change-Id: Ib975d5a7d3d687b5978d3eddcfa22a9bfef97236 --- .../CommunalTrustedNetworkCondition.java | 73 ++++++++++++++++++- .../CommunalTrustedNetworkConditionTest.java | 72 ++++++++++++++++-- 2 files changed, 136 insertions(+), 9 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalTrustedNetworkCondition.java b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalTrustedNetworkCondition.java index c817979ee3c5a..e4692dbba1ca6 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalTrustedNetworkCondition.java +++ b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalTrustedNetworkCondition.java @@ -16,15 +16,26 @@ package com.android.systemui.communal.conditions; +import android.database.ContentObserver; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.wifi.WifiInfo; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.util.settings.SecureSettings; + +import java.util.Arrays; +import java.util.HashSet; + import javax.inject.Inject; /** @@ -34,10 +45,32 @@ import javax.inject.Inject; public class CommunalTrustedNetworkCondition extends CommunalCondition { private final String mTag = getClass().getSimpleName(); private final ConnectivityManager mConnectivityManager; + private final ContentObserver mTrustedNetworksObserver; + private final SecureSettings mSecureSettings; + + // The SSID of the connected Wi-Fi network. Null if not connected to Wi-Fi. + private String mWifiSSID; + + // Set of SSIDs of trusted networks. + private final HashSet mTrustedNetworks = new HashSet<>(); + + /** + * The deliminator used to separate trusted network keys saved as a string in secure settings. + */ + public static final String SETTINGS_STRING_DELIMINATOR = ",/"; @Inject - public CommunalTrustedNetworkCondition(ConnectivityManager connectivityManager) { + public CommunalTrustedNetworkCondition(@Main Handler handler, + ConnectivityManager connectivityManager, SecureSettings secureSettings) { mConnectivityManager = connectivityManager; + mSecureSettings = secureSettings; + + mTrustedNetworksObserver = new ContentObserver(handler) { + @Override + public void onChange(boolean selfChange) { + fetchTrustedNetworks(); + } + }; } /** @@ -47,9 +80,14 @@ public class CommunalTrustedNetworkCondition extends CommunalCondition { protected void start() { if (shouldLog()) Log.d(mTag, "start listening for wifi connections"); + fetchTrustedNetworks(); + final NetworkRequest wifiNetworkRequest = new NetworkRequest.Builder().addTransportType( NetworkCapabilities.TRANSPORT_WIFI).build(); mConnectivityManager.registerNetworkCallback(wifiNetworkRequest, mNetworkCallback); + mSecureSettings.registerContentObserverForUser( + Settings.Secure.COMMUNAL_MODE_TRUSTED_NETWORKS, false, mTrustedNetworksObserver, + UserHandle.USER_SYSTEM); } /** @@ -60,11 +98,40 @@ public class CommunalTrustedNetworkCondition extends CommunalCondition { if (shouldLog()) Log.d(mTag, "stop listening for wifi connections"); mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + mSecureSettings.unregisterContentObserver(mTrustedNetworksObserver); } private void updateWifiInfo(WifiInfo wifiInfo) { - // TODO(b/202778351): check whether wifi is trusted. - final boolean connectedToTrustedNetwork = wifiInfo != null; + if (wifiInfo == null) { + mWifiSSID = null; + } else { + // Remove the wrapping quotes around the SSID. + mWifiSSID = wifiInfo.getSSID().replace("\"", ""); + } + + checkIfConnectedToTrustedNetwork(); + } + + private void fetchTrustedNetworks() { + final String trustedNetworksString = mSecureSettings.getStringForUser( + Settings.Secure.COMMUNAL_MODE_TRUSTED_NETWORKS, UserHandle.USER_SYSTEM); + mTrustedNetworks.clear(); + + if (shouldLog()) Log.d(mTag, "fetched trusted networks: " + trustedNetworksString); + + if (TextUtils.isEmpty(trustedNetworksString)) { + return; + } + + mTrustedNetworks.addAll( + Arrays.asList(trustedNetworksString.split(SETTINGS_STRING_DELIMINATOR))); + + checkIfConnectedToTrustedNetwork(); + } + + private void checkIfConnectedToTrustedNetwork() { + final boolean connectedToTrustedNetwork = mWifiSSID != null && mTrustedNetworks.contains( + mWifiSSID); if (shouldLog()) { Log.d(mTag, (connectedToTrustedNetwork ? "connected to" : "disconnected from") diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalTrustedNetworkConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalTrustedNetworkConditionTest.java index a00c12b113a17..61a5126fb139a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalTrustedNetworkConditionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalTrustedNetworkConditionTest.java @@ -16,9 +16,12 @@ package com.android.systemui.communal; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -27,12 +30,18 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.wifi.WifiInfo; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.communal.conditions.CommunalTrustedNetworkCondition; +import com.android.systemui.util.settings.FakeSettings; +import com.android.systemui.utils.os.FakeHandler; import org.junit.Before; import org.junit.Test; @@ -49,12 +58,21 @@ public class CommunalTrustedNetworkConditionTest extends SysuiTestCase { @Captor private ArgumentCaptor mNetworkCallbackCaptor; + private final Handler mHandler = new FakeHandler(Looper.getMainLooper()); private CommunalTrustedNetworkCondition mCondition; + private final String mTrustedWifi1 = "wifi-1"; + private final String mTrustedWifi2 = "wifi-2"; + @Before public void setup() { MockitoAnnotations.initMocks(this); - mCondition = new CommunalTrustedNetworkCondition(mConnectivityManager); + final FakeSettings secureSettings = new FakeSettings(); + secureSettings.putStringForUser(Settings.Secure.COMMUNAL_MODE_TRUSTED_NETWORKS, + mTrustedWifi1 + CommunalTrustedNetworkCondition.SETTINGS_STRING_DELIMINATOR + + mTrustedWifi2, UserHandle.USER_SYSTEM); + mCondition = new CommunalTrustedNetworkCondition(mHandler, mConnectivityManager, + secureSettings); } @Test @@ -65,17 +83,58 @@ public class CommunalTrustedNetworkConditionTest extends SysuiTestCase { final ConnectivityManager.NetworkCallback networkCallback = captureNetworkCallback(); - // Connected to Wi-Fi. + // Connected to trusted Wi-Fi network. final Network network = mock(Network.class); networkCallback.onAvailable(network); - networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities()); + networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities(mTrustedWifi1)); // Verifies that the callback is triggered. verify(callback).onConditionChanged(mCondition, true); } @Test - public void updateCallback_disconnectedFromTrustedNetwork_reportsFalse() { + public void updateCallback_switchedToAnotherTrustedNetwork_reportsNothing() { + final CommunalTrustedNetworkCondition.Callback callback = + mock(CommunalTrustedNetworkCondition.Callback.class); + mCondition.addCallback(callback); + + final ConnectivityManager.NetworkCallback networkCallback = captureNetworkCallback(); + + // Connected to a trusted Wi-Fi network. + final Network network = mock(Network.class); + networkCallback.onAvailable(network); + networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities(mTrustedWifi1)); + clearInvocations(callback); + + // Connected to another trusted Wi-Fi network. + networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities(mTrustedWifi2)); + + // Verifies that the callback is not triggered. + verify(callback, never()).onConditionChanged(eq(mCondition), anyBoolean()); + } + + @Test + public void updateCallback_connectedToNonTrustedNetwork_reportsFalse() { + final CommunalTrustedNetworkCondition.Callback callback = + mock(CommunalTrustedNetworkCondition.Callback.class); + mCondition.addCallback(callback); + + final ConnectivityManager.NetworkCallback networkCallback = captureNetworkCallback(); + + // Connected to trusted Wi-Fi network. + final Network network = mock(Network.class); + networkCallback.onAvailable(network); + networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities(mTrustedWifi1)); + + // Connected to non-trusted Wi-Fi network. + networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities("random-wifi")); + + // Verifies that the callback is triggered. + verify(callback).onConditionChanged(mCondition, false); + } + + @Test + public void updateCallback_disconnectedFromNetwork_reportsFalse() { final CommunalTrustedNetworkCondition.Callback callback = mock(CommunalTrustedNetworkCondition.Callback.class); mCondition.addCallback(callback); @@ -85,7 +144,7 @@ public class CommunalTrustedNetworkConditionTest extends SysuiTestCase { // Connected to Wi-Fi. final Network network = mock(Network.class); networkCallback.onAvailable(network); - networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities()); + networkCallback.onCapabilitiesChanged(network, fakeNetworkCapabilities(mTrustedWifi1)); clearInvocations(callback); // Disconnected from Wi-Fi. @@ -103,9 +162,10 @@ public class CommunalTrustedNetworkConditionTest extends SysuiTestCase { return mNetworkCallbackCaptor.getValue(); } - private NetworkCapabilities fakeNetworkCapabilities() { + private NetworkCapabilities fakeNetworkCapabilities(String ssid) { final NetworkCapabilities networkCapabilities = mock(NetworkCapabilities.class); final WifiInfo wifiInfo = mock(WifiInfo.class); + when(wifiInfo.getSSID()).thenReturn(ssid); when(networkCapabilities.getTransportInfo()).thenReturn(wifiInfo); return networkCapabilities; }