Show icons for apps bypassing DND

(Several things pending, such as the +n icon and the correct pending icons, but it's a start).

Test: atest com.android.settings.notification.modes
Bug: 346551087
Flag: android.app.modes_ui
Change-Id: Ifd2ab6a8bb447739dc8ffe400c3960779d477fd6
This commit is contained in:
Matías Hernández
2024-07-15 17:47:21 +02:00
parent fe712d3ea3
commit 0ebc865c5e
15 changed files with 832 additions and 59 deletions

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2024 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.settings.notification.modes;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import java.util.List;
import java.util.function.Function;
@RunWith(RobolectricTestRunner.class)
public class CircularIconSetTest {
@Mock private Function<Integer, Drawable> mDrawableLoader;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
when(mDrawableLoader.apply(anyInt())).thenReturn(new ColorDrawable(Color.BLACK));
}
@Test
public void getIcons_loadsAllIcons() {
CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
mDrawableLoader);
List<ListenableFuture<Drawable>> iconFutures = set.getIcons();
assertThat(iconFutures).hasSize(3);
verify(mDrawableLoader).apply(1);
verify(mDrawableLoader).apply(2);
verify(mDrawableLoader).apply(3);
}
@Test
public void getIcons_loadsRequestedIcons() {
CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3, 4, 5),
mDrawableLoader);
List<ListenableFuture<Drawable>> iconFutures = set.getIcons(2);
assertThat(iconFutures).hasSize(2);
verify(mDrawableLoader).apply(1);
verify(mDrawableLoader).apply(2);
verifyNoMoreInteractions(mDrawableLoader);
}
@Test
public void getIcons_cachesIcons() {
CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3, 4, 5),
mDrawableLoader);
List<ListenableFuture<Drawable>> iconFutures = set.getIcons(2);
assertThat(iconFutures).hasSize(2);
verify(mDrawableLoader).apply(1);
verify(mDrawableLoader).apply(2);
verifyNoMoreInteractions(mDrawableLoader);
List<ListenableFuture<Drawable>> iconFuturesAgain = set.getIcons(3);
assertThat(iconFuturesAgain).hasSize(3);
verify(mDrawableLoader).apply(3);
verifyNoMoreInteractions(mDrawableLoader);
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (C) 2024 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.settings.notification.modes;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.List;
import java.util.stream.IntStream;
@RunWith(RobolectricTestRunner.class)
public class CircularIconsPreferenceTest {
private static final int VIEW_WIDTH = 800;
private Context mContext;
private CircularIconsPreference mPreference;
private View mIconContainer;
private int mOneIconWidth;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
mPreference = new CircularIconsPreference(mContext, MoreExecutors.directExecutor());
// Tests should call bindAndMeasureViewHolder() so that icons can be added.
Resources res = mContext.getResources();
mOneIconWidth = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_size)
+ res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
}
private void bindAndMeasureViewHolder(int viewWidth) {
View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(),
null);
mIconContainer = checkNotNull(preferenceView.findViewById(R.id.circles_container));
mIconContainer.measure(makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY),
makeMeasureSpec(1000, View.MeasureSpec.EXACTLY));
PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView);
mPreference.onBindViewHolder(holder);
}
@Test
public void displayIcons_loadsIcons() {
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2),
ColorDrawable::new);
bindAndMeasureViewHolder(VIEW_WIDTH);
mPreference.displayIcons(iconSet);
assertThat(mPreference.getIconViews()).hasSize(2);
assertThat(mPreference.getIconViews().get(0).getDrawable())
.isInstanceOf(ColorDrawable.class);
assertThat(((ColorDrawable) mPreference.getIconViews().get(0).getDrawable()).getColor())
.isEqualTo(1);
assertThat(((ColorDrawable) mPreference.getIconViews().get(1).getDrawable()).getColor())
.isEqualTo(2);
assertThat(mIconContainer.getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void displayIcons_noIcons_hidesRow() {
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(),
ColorDrawable::new);
bindAndMeasureViewHolder(VIEW_WIDTH);
mPreference.displayIcons(iconSet);
assertThat(mIconContainer.getVisibility()).isEqualTo(View.GONE);
}
@Test
public void displayIcons_exactlyMaxIcons_loadsAllIcons() throws Exception {
int width = 300;
int fittingIcons = width / mOneIconWidth;
CircularIconSet<Integer> iconSet = new CircularIconSet<>(
IntStream.range(0, fittingIcons).boxed().toList(),
ColorDrawable::new);
bindAndMeasureViewHolder(width);
mPreference.displayIcons(iconSet);
List<Drawable> displayedDrawables = mPreference.getIconViews().stream()
.map(ImageView::getDrawable).toList();
assertThat(displayedDrawables).hasSize(fittingIcons);
assertThat(displayedDrawables).containsExactlyElementsIn(
Futures.allAsList(iconSet.getIcons()).get()).inOrder();
}
@Test
public void displayIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception {
int width = 300;
int fittingIcons = width / mOneIconWidth;
CircularIconSet<Integer> iconSet = new CircularIconSet<>(
IntStream.range(0, fittingIcons + 5).boxed().toList(),
ColorDrawable::new);
bindAndMeasureViewHolder(width);
mPreference.displayIcons(iconSet);
List<Drawable> displayedDrawables = mPreference.getIconViews().stream()
.map(ImageView::getDrawable).toList();
assertThat(displayedDrawables).hasSize(fittingIcons);
// N-1 are actual icons, Nth icon is (+xx).
assertThat(displayedDrawables.stream().limit(fittingIcons - 1).toList())
.containsExactlyElementsIn(
Futures.allAsList(iconSet.getIcons(fittingIcons - 1)).get())
.inOrder();
// TODO: b/346551087 - Correctly verify the plus-6 icon, once we generate it properly.
assertThat(((ColorDrawable) displayedDrawables.get(
displayedDrawables.size() - 1)).getColor()).isEqualTo(Color.BLUE);
}
@Test
public void displayIcons_teenyTinySpace_showsPlusIcon_noCrash() {
int width = 1;
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2),
ColorDrawable::new);
bindAndMeasureViewHolder(width);
mPreference.displayIcons(iconSet);
assertThat(mPreference.getIconViews()).hasSize(1);
// TODO: b/346551087 - Correctly verify the plus-2 icon, once we generate it properly.
assertThat(((ColorDrawable) mPreference.getIconViews().get(0).getDrawable()).getColor())
.isEqualTo(Color.BLUE);
}
@Test
public void displayIcons_beforeBind_loadsIconsOnBind() {
CircularIconSet<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new);
mPreference.displayIcons(iconSet);
assertThat(mPreference.getIconViews()).isEmpty();
bindAndMeasureViewHolder(VIEW_WIDTH);
assertThat(mPreference.getIconViews()).hasSize(3);
}
@Test
public void displayIcons_calledAgain_reloadsIcons() {
CircularIconSet<Integer> threeIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new);
CircularIconSet<Integer> twoIcons = new CircularIconSet<>(ImmutableList.of(1, 2),
ColorDrawable::new);
CircularIconSet<Integer> fourIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3, 4),
ColorDrawable::new);
bindAndMeasureViewHolder(VIEW_WIDTH);
mPreference.displayIcons(threeIcons);
assertThat(mPreference.getIconViews()).hasSize(3);
mPreference.displayIcons(twoIcons);
assertThat(mPreference.getIconViews()).hasSize(2);
mPreference.displayIcons(fourIcons);
assertThat(mPreference.getIconViews()).hasSize(4);
}
}

View File

@@ -23,10 +23,13 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
@@ -41,8 +44,11 @@ import android.os.UserManager;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import android.view.LayoutInflater;
import android.view.View;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.SettingsActivity;
import com.android.settingslib.applications.ApplicationsState;
@@ -51,7 +57,9 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.notification.modes.TestModeBuilder;
import com.android.settingslib.notification.modes.ZenMode;
import com.android.settingslib.notification.modes.ZenModesBackend;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before;
import org.junit.Rule;
@@ -71,6 +79,7 @@ import java.util.Random;
public final class ZenModeAppsLinkPreferenceControllerTest {
private ZenModeAppsLinkPreferenceController mController;
private CircularIconsPreference mPreference;
private Context mContext;
@Mock
@@ -91,10 +100,21 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
mPreference = new CircularIconsPreference(mContext, MoreExecutors.directExecutor());
when(mApplicationsState.newSession(any(), any())).thenReturn(mSession);
mController = new ZenModeAppsLinkPreferenceController(
mContext, "controller_key", mock(Fragment.class), mApplicationsState,
mZenModesBackend, mHelperBackend);
// Ensure the preference view is bound & measured (needed to add child ImageViews).
View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(),
null);
preferenceView.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY));
PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView);
mPreference.onBindViewHolder(holder);
}
private AppEntry createAppEntry(String packageName, int userId) {
@@ -123,13 +143,11 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
@Test
public void testUpdateSetsIntent() {
// Creates the preference
SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference(mContext);
// Create a zen mode that allows priority channels to breakthrough.
ZenMode zenMode = createPriorityChannelsZenMode();
mController.updateState(preference, zenMode);
Intent launcherIntent = preference.getIntent();
mController.updateState(mPreference, zenMode);
Intent launcherIntent = mPreference.getIntent();
assertThat(launcherIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
.isEqualTo("com.android.settings.notification.modes.ZenModeAppsFragment");
@@ -193,9 +211,20 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
}
@Test
public void testUpdateTriggersRebuild() {
// Creates the preference
SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference(mContext);
public void updateState_withPolicyAllowingNoChannels_doesNotLoadPriorityApps() {
ZenMode zenMode = new TestModeBuilder()
.setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
.build();
mController.updateState(mPreference, zenMode);
verifyNoMoreInteractions(mSession);
verify(mHelperBackend, never()).getPackagesBypassingDnd(anyInt(), anyBoolean());
assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("None");
}
@Test
public void updateState_withPolicyAllowingPriorityChannels_triggersRebuild() {
// Create a zen mode that allows priority channels to breakthrough.
ZenMode zenMode = createPriorityChannelsZenMode();
@@ -209,21 +238,35 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
// Updates the preference with the zen mode. We expect that this causes the app session
// to trigger a rebuild (and display a temporary text in the meantime).
mController.updateZenMode(preference, zenMode);
mController.updateZenMode(mPreference, zenMode);
verify(mSession).rebuild(any(), any(), eq(false));
assertThat(String.valueOf(preference.getSummary())).isEqualTo("Calculating…");
assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("Calculating…");
// Manually triggers the callback that will happen on rebuild.
mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
assertThat(String.valueOf(preference.getSummary())).isEqualTo("test can interrupt");
assertThat(String.valueOf(mPreference.getSummary())).isEqualTo("test can interrupt");
}
@Test
public void updateState_withPolicyAllowingPriorityChannels_loadsIcons() {
ZenMode zenMode = createPriorityChannelsZenMode();
mController.updateState(mPreference, zenMode);
when(mHelperBackend.getPackagesBypassingDnd(anyInt(), anyBoolean()))
.thenReturn(ImmutableList.of("test1", "test2"));
ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
appEntries.add(createAppEntry("test1", mContext.getUserId()));
appEntries.add(createAppEntry("test2", mContext.getUserId()));
mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
assertThat(mPreference.getIconViews()).hasSize(2);
}
@Test
public void testOnPackageListChangedTriggersRebuild() {
SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference(mContext);
// Create a zen mode that allows priority channels to breakthrough.
ZenMode zenMode = createPriorityChannelsZenMode();
mController.updateState(preference, zenMode);
mController.updateState(mPreference, zenMode);
verify(mSession).rebuild(any(), any(), eq(false));
mController.mAppSessionCallbacks.onPackageListChanged();
@@ -232,10 +275,9 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
@Test
public void testOnLoadEntriesCompletedTriggersRebuild() {
SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference(mContext);
// Create a zen mode that allows priority channels to breakthrough.
ZenMode zenMode = createPriorityChannelsZenMode();
mController.updateState(preference, zenMode);
mController.updateState(mPreference, zenMode);
verify(mSession).rebuild(any(), any(), eq(false));
mController.mAppSessionCallbacks.onLoadEntriesCompleted();

View File

@@ -17,6 +17,7 @@
package com.android.settings.notification.modes;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -25,8 +26,6 @@ import android.content.Context;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.preference.Preference;
import com.android.settingslib.notification.modes.TestModeBuilder;
import org.junit.Before;
@@ -63,8 +62,11 @@ public final class ZenModeOtherLinkPreferenceControllerTest {
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
public void testHasSummary() {
Preference pref = mock(Preference.class);
CircularIconsPreference pref = mock(CircularIconsPreference.class);
mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
verify(pref).setSummary(any());
verify(pref).displayIcons(eq(CircularIconSet.EMPTY));
}
}

View File

@@ -17,6 +17,7 @@
package com.android.settings.notification.modes;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -25,8 +26,6 @@ import android.content.Context;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.preference.Preference;
import com.android.settingslib.notification.modes.TestModeBuilder;
import org.junit.Before;
@@ -63,8 +62,11 @@ public final class ZenModePeopleLinkPreferenceControllerTest {
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
public void testHasSummary() {
Preference pref = mock(Preference.class);
CircularIconsPreference pref = mock(CircularIconsPreference.class);
mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
verify(pref).setSummary(any());
verify(pref).displayIcons(eq(CircularIconSet.EMPTY));
}
}