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
This commit is contained in:
Darrell Shi
2021-11-19 18:46:39 +00:00
parent fb6d7eaa1d
commit c960b0b22b
6 changed files with 646 additions and 0 deletions

View File

@@ -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<CommunalCondition.Callback> {
private final String mTag = getClass().getSimpleName();
private final ArrayList<WeakReference<Callback>> 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<WeakReference<Callback>> 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<WeakReference<Callback>> 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);
}
}

View File

@@ -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<CommunalConditionsMonitor.Callback> {
private final String mTag = getClass().getSimpleName();
private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
// Set of all conditions that need to be monitored.
private final Set<CommunalCondition> mConditions;
// Map of values of each condition.
private final HashMap<CommunalCondition, Boolean> 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<WeakReference<Callback>> 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<CommunalCondition> 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<WeakReference<Callback>> 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);
}
}

View File

@@ -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<CommunalCondition> provideCommunalConditions() {
return new HashSet<>();
}
}

View File

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

View File

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

View File

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