From 352100397f21bdd215d208559fd9f96ff09ec1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Thu, 18 Jul 2024 11:32:21 +0200 Subject: [PATCH] Show icons for allowed sounds (Some icons are temporary, until we get the final assets) Also some fixes/improvements to CircularIconsPreference: * Show the correct placeholder and +N icons. * Fix the displayIcons-before-measure case (global layout listener was incorrect). * Properly cancel pending image load futures (field didn't point to the actual future). * Don't reload icons if it's the same set (depends on equals() for the items, so unfortunately doesn't work for AppEntry yet). Test: atest com.android.settings.notification.modes Bug: 346551087 Flag: android.app.modes_ui Change-Id: I9d029a5fdd785ada4e2ba4d8a90eba72b5fb9085 --- ...ce_circular_icons_plus_item_background.xml | 28 ++++ res/layout/preference_circular_icons_item.xml | 4 +- .../preference_circular_icons_plus_item.xml | 32 ++++ res/values/dimens.xml | 3 +- res/values/strings.xml | 2 + .../notification/modes/CircularIconSet.java | 10 ++ .../modes/CircularIconsPreference.java | 141 +++++++++++++----- .../settings/notification/modes/IconUtil.java | 24 ++- ...odeIconPickerIconPreferenceController.java | 2 +- ...odeIconPickerListPreferenceController.java | 2 +- .../ZenModeOtherLinkPreferenceController.java | 39 ++++- .../modes/ZenModeSummaryHelper.java | 16 +- .../modes/CircularIconSetTest.java | 30 ++++ .../modes/CircularIconsPreferenceTest.java | 120 +++++++++------ ...nModeAppsLinkPreferenceControllerTest.java | 2 +- ...ModeOtherLinkPreferenceControllerTest.java | 39 ++++- 16 files changed, 389 insertions(+), 105 deletions(-) create mode 100644 res/drawable/preference_circular_icons_plus_item_background.xml create mode 100644 res/layout/preference_circular_icons_plus_item.xml diff --git a/res/drawable/preference_circular_icons_plus_item_background.xml b/res/drawable/preference_circular_icons_plus_item_background.xml new file mode 100644 index 00000000000..8200a9b12eb --- /dev/null +++ b/res/drawable/preference_circular_icons_plus_item_background.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/preference_circular_icons_item.xml b/res/layout/preference_circular_icons_item.xml index 3e8d7fa1f2b..e5656ce599b 100644 --- a/res/layout/preference_circular_icons_item.xml +++ b/res/layout/preference_circular_icons_item.xml @@ -17,8 +17,8 @@ \ No newline at end of file diff --git a/res/layout/preference_circular_icons_plus_item.xml b/res/layout/preference_circular_icons_plus_item.xml new file mode 100644 index 00000000000..98820863410 --- /dev/null +++ b/res/layout/preference_circular_icons_plus_item.xml @@ -0,0 +1,32 @@ + + + + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index c76fff5965e..2bb8fc2ba0f 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -509,7 +509,8 @@ 56dp 32dp - 32dp + 32dp + 20dp 4dp 8dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 05fd19a31e9..ecfc10a3458 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9361,6 +9361,8 @@ %s (Work) Calculating\u2026 + + +%d Allow apps to override diff --git a/src/com/android/settings/notification/modes/CircularIconSet.java b/src/com/android/settings/notification/modes/CircularIconSet.java index 55a92fd749c..18f82d97de0 100644 --- a/src/com/android/settings/notification/modes/CircularIconSet.java +++ b/src/com/android/settings/notification/modes/CircularIconSet.java @@ -22,6 +22,7 @@ import android.graphics.drawable.Drawable; import androidx.annotation.VisibleForTesting; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -61,6 +62,15 @@ class CircularIconSet { mCachedIcons = new ConcurrentHashMap<>(); } + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("items", mItems).toString(); + } + + boolean hasSameItemsAs(CircularIconSet other) { + return other != null && this.mItems.equals(other.mItems); + } + int size() { return mItems.size(); } diff --git a/src/com/android/settings/notification/modes/CircularIconsPreference.java b/src/com/android/settings/notification/modes/CircularIconsPreference.java index 1f6e0b0ef34..5e8f720c83c 100644 --- a/src/com/android/settings/notification/modes/CircularIconsPreference.java +++ b/src/com/android/settings/notification/modes/CircularIconsPreference.java @@ -21,15 +21,17 @@ import static com.google.common.base.Preconditions.checkState; 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.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.TextView; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -37,11 +39,12 @@ import androidx.preference.PreferenceViewHolder; import com.android.settings.R; import com.android.settingslib.RestrictedPreference; +import com.android.settingslib.Utils; -import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; @@ -50,8 +53,9 @@ public class CircularIconsPreference extends RestrictedPreference { private Executor mUiExecutor; @Nullable private LinearLayout mIconContainer; - @Nullable private CircularIconSet mPendingIconSet; - @Nullable private ListenableFuture mPendingLoadIconsFuture; + @Nullable private CircularIconSet mIconSet; + @Nullable private CircularIconSet mPendingDisplayIconSet; + @Nullable private ListenableFuture> mPendingLoadIconsFuture; public CircularIconsPreference(Context context) { super(context); @@ -94,17 +98,25 @@ public class CircularIconsPreference extends RestrictedPreference { } private void displayIconsIfPending() { - CircularIconSet pendingIconSet = mPendingIconSet; + CircularIconSet pendingIconSet = mPendingDisplayIconSet; if (pendingIconSet != null) { - mPendingIconSet = null; - displayIcons(pendingIconSet); + mPendingDisplayIconSet = null; + displayIconsInternal(pendingIconSet); } } void displayIcons(CircularIconSet iconSet) { + if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet)) { + return; + } + mIconSet = iconSet; + displayIconsInternal(iconSet); + } + + void displayIconsInternal(CircularIconSet iconSet) { if (mIconContainer == null) { // Too soon, wait for bind. - mPendingIconSet = iconSet; + mPendingDisplayIconSet = iconSet; return; } mIconContainer.setVisibility(iconSet.size() != 0 ? View.VISIBLE : View.GONE); @@ -113,30 +125,31 @@ public class CircularIconsPreference extends RestrictedPreference { } if (mIconContainer.getMeasuredWidth() == 0) { // Too soon, wait for first measure to know width. - mPendingIconSet = iconSet; - ViewTreeObserver vto = mIconContainer.getViewTreeObserver(); - vto.addOnGlobalLayoutListener(() -> + mPendingDisplayIconSet = iconSet; + mIconContainer.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { - vto.removeOnGlobalLayoutListener(this); + checkNotNull(mIconContainer).getViewTreeObserver() + .removeOnGlobalLayoutListener(this); displayIconsIfPending(); } - }); + } + ); return; } mIconContainer.setVisibility(View.VISIBLE); Resources res = getContext().getResources(); int availableSpace = mIconContainer.getMeasuredWidth(); - int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_size) + int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); int numIconsThatFit = availableSpace / iconHorizontalSpace; List> iconFutures; - int extraItems = 0; + int extraItems; if (iconSet.size() > numIconsThatFit) { - // Reserve one space for the (+xx) circle. + // Reserve one space for the (+xx) textview. int numIconsToShow = numIconsThatFit - 1; if (numIconsToShow < 0) { numIconsToShow = 0; @@ -146,6 +159,7 @@ public class CircularIconsPreference extends RestrictedPreference { } else { // Fit exactly or with remaining space. iconFutures = iconSet.getIcons(); + extraItems = 0; } displayIconsWhenReady(iconFutures, extraItems); @@ -158,33 +172,45 @@ public class CircularIconsPreference extends RestrictedPreference { mPendingLoadIconsFuture.cancel(true); } - int numCircles = iconFutures.size() + (extraItems > 0 ? 1 : 0); - if (mIconContainer.getChildCount() > numCircles) { - mIconContainer.removeViews(numCircles, mIconContainer.getChildCount() - numCircles); + // Rearrange child views until we have ImageViews... + LayoutInflater inflater = LayoutInflater.from(getContext()); + int numImages = iconFutures.size(); + int numImageViews = getChildCount(mIconContainer, ImageView.class); + if (numImages > numImageViews) { + for (int i = 0; i < numImages - numImageViews; i++) { + ImageView imageView = (ImageView) inflater.inflate( + R.layout.preference_circular_icons_item, mIconContainer, false); + mIconContainer.addView(imageView, 0); + } + } else if (numImageViews > numImages) { + for (int i = 0; i < numImageViews - numImages; i++) { + mIconContainer.removeViewAt(0); + } } - for (int i = mIconContainer.getChildCount(); i < numCircles; i++) { - ImageView imageView = (ImageView) LayoutInflater.from(getContext()).inflate( - R.layout.preference_circular_icons_item, mIconContainer, false); - mIconContainer.addView(imageView); + // ... plus 0/1 TextViews at the end. + if (extraItems > 0 && !(getLastChild(mIconContainer) instanceof TextView)) { + // TODO: b/346551087 - Check TODO in preference_circular_icons_plus_item_background + TextView plusView = (TextView) inflater.inflate( + R.layout.preference_circular_icons_plus_item, mIconContainer, false); + mIconContainer.addView(plusView); + } else if (extraItems == 0 && (getLastChild(mIconContainer) instanceof TextView)) { + mIconContainer.removeViewAt(mIconContainer.getChildCount() - 1); } // Set up placeholders and extra items indicator. - for (int i = 0; i < iconFutures.size(); i++) { + for (int i = 0; i < numImages; i++) { ImageView imageView = (ImageView) mIconContainer.getChildAt(i); - // TODO: b/346551087 - proper color and shape, should be a gray circle. - imageView.setImageDrawable(new ColorDrawable(Color.RED)); + imageView.setImageDrawable(getPlaceholderImage(getContext())); } if (extraItems > 0) { - ImageView imageView = (ImageView) mIconContainer.getChildAt( - mIconContainer.getChildCount() - 1); - // TODO: b/346551087 - proper color and shape and number. - imageView.setImageDrawable(new ColorDrawable(Color.BLUE)); + TextView textView = (TextView) checkNotNull(getLastChild(mIconContainer)); + textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, extraItems)); } // Display icons when all are ready (more consistent than randomly loading). mPendingLoadIconsFuture = Futures.allAsList(iconFutures); FutureUtil.whenDone( - Futures.allAsList(iconFutures), + mPendingLoadIconsFuture, icons -> { checkState(mIconContainer != null); for (int i = 0; i < icons.size(); i++) { @@ -194,15 +220,54 @@ public class CircularIconsPreference extends RestrictedPreference { mUiExecutor); } + private static Drawable getPlaceholderImage(Context context) { + ShapeDrawable placeholder = new ShapeDrawable(new OvalShape()); + placeholder.setTintList(Utils.getColorAttr(context, + com.android.internal.R.attr.materialColorSecondaryContainer)); + return placeholder; + } + + private static int getChildCount(ViewGroup parent, Class childClass) { + int count = 0; + for (int i = 0; i < parent.getChildCount(); i++) { + if (childClass.isInstance(parent.getChildAt(i))) { + count++; + } + } + return count; + } + + @Nullable + private static View getLastChild(ViewGroup parent) { + if (parent.getChildCount() == 0) { + return null; + } + return parent.getChildAt(parent.getChildCount() - 1); + } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) - ImmutableList getIconViews() { + List getIcons() { if (mIconContainer == null) { - return ImmutableList.of(); + return List.of(); } - ImmutableList.Builder imageViews = new ImmutableList.Builder<>(); - for (int i = 0; i < mIconContainer.getChildCount(); i++) { - imageViews.add((ImageView) mIconContainer.getChildAt(i)); + ArrayList drawables = new ArrayList<>(); + for (int i = 0; i < getChildCount(mIconContainer, ImageView.class); i++) { + drawables.add(((ImageView) mIconContainer.getChildAt(i)).getDrawable()); + } + return drawables; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @Nullable + String getPlusText() { + if (mIconContainer == null) { + return null; + } + View lastChild = getLastChild(mIconContainer); + if (lastChild instanceof TextView tv) { + return tv.getText() != null ? tv.getText().toString() : null; + } else { + return null; } - return imageViews.build(); } } diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java index d07abf34b79..07e14407353 100644 --- a/src/com/android/settings/notification/modes/IconUtil.java +++ b/src/com/android/settings/notification/modes/IconUtil.java @@ -55,7 +55,7 @@ class IconUtil { * Returns a variant of the supplied {@code icon} to be used as the header in the icon picker. * The inner icon is 48x48dp and it's contained into a circle of diameter 90dp. */ - static Drawable makeBigIconCircle(@NonNull Context context, Drawable icon) { + static Drawable makeIconPickerHeader(@NonNull Context context, Drawable icon) { return composeIconCircle( Utils.getColorAttr(context, com.android.internal.R.attr.materialColorSecondaryContainer), @@ -73,7 +73,7 @@ class IconUtil { * The inner icon is 36x36dp and it's contained into a circle of diameter 54dp. It's also set up * so that selection and pressed states are represented in the color. */ - static Drawable makeSmallIconCircle(@NonNull Context context, @DrawableRes int iconResId) { + static Drawable makeIconPickerItem(@NonNull Context context, @DrawableRes int iconResId) { return composeIconCircle( context.getColorStateList(R.color.modes_icon_picker_item_background), context.getResources().getDimensionPixelSize( @@ -84,6 +84,24 @@ class IconUtil { R.dimen.zen_mode_icon_list_item_icon_size)); } + /** + * Returns a variant of the supplied icon to be used in a {@link CircularIconsPreference}. The + * inner icon is 20x20 dp and it's contained in a circle of diameter 32dp, and is tinted + * with the "material secondary container" color combination. + */ + static Drawable makeSoundIcon(@NonNull Context context, @DrawableRes int iconResId) { + return composeIconCircle( + Utils.getColorAttr(context, + com.android.internal.R.attr.materialColorSecondaryContainer), + context.getResources().getDimensionPixelSize( + R.dimen.zen_mode_circular_icon_diameter), + checkNotNull(context.getDrawable(iconResId)), + Utils.getColorAttr(context, + com.android.internal.R.attr.materialColorOnSecondaryContainer), + context.getResources().getDimensionPixelSize( + R.dimen.zen_mode_circular_icon_inner_icon_size)); + } + private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { ShapeDrawable background = new ShapeDrawable(new OvalShape()); @@ -93,11 +111,11 @@ class IconUtil { LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground }); - layerDrawable.setBounds(0, 0, circleDiameterPx, circleDiameterPx); layerDrawable.setLayerSize(0, circleDiameterPx, circleDiameterPx); layerDrawable.setLayerGravity(1, Gravity.CENTER); layerDrawable.setLayerSize(1, iconSizePx, iconSizePx); + layerDrawable.setBounds(0, 0, circleDiameterPx, circleDiameterPx); return layerDrawable; } } diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java index 70df9b651ee..1b51cfa6c35 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java @@ -64,7 +64,7 @@ class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenc FutureUtil.whenDone( zenMode.getIcon(mContext, ZenIconLoader.getInstance()), - icon -> mHeaderController.setIcon(IconUtil.makeBigIconCircle(mContext, icon)) + icon -> mHeaderController.setIcon(IconUtil.makeIconPickerHeader(mContext, icon)) .done(/* rebindActions= */ false), mContext.getMainExecutor()); } diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java index 512dabb4437..93df38b61a5 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java @@ -156,7 +156,7 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc public void onBindViewHolder(@NonNull IconHolder holder, int position) { IconOptionsProvider.IconInfo iconInfo = mIconResources.get(position); Drawable iconDrawable = mIconCache.computeIfAbsent(iconInfo, - info -> IconUtil.makeSmallIconCircle(mContext, info.resId())); + info -> IconUtil.makeIconPickerItem(mContext, info.resId())); holder.bindIcon(iconInfo, iconDrawable); } diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java index 452faed3f8d..fce48afe443 100644 --- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java @@ -17,19 +17,44 @@ package com.android.settings.notification.modes; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; import android.content.Context; +import android.service.notification.ZenPolicy; import androidx.annotation.NonNull; import androidx.preference.Preference; import com.android.settingslib.notification.modes.ZenMode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + /** * Preference with a link and summary about what other sounds can break through the mode */ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController { + // TODO: b/346551087 - Use proper icons + private static final ImmutableMap + PRIORITIES_TO_ICONS = ImmutableMap.of( + PRIORITY_CATEGORY_ALARMS, + com.android.internal.R.drawable.ic_audio_alarm, + PRIORITY_CATEGORY_MEDIA, + com.android.settings.R.drawable.ic_media_stream, + PRIORITY_CATEGORY_SYSTEM, + com.android.settings.R.drawable.ic_settings_keyboards, + PRIORITY_CATEGORY_REMINDERS, + com.android.internal.R.drawable.ic_popup_reminder, + PRIORITY_CATEGORY_EVENTS, + com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar); + private final ZenModeSummaryHelper mSummaryHelper; public ZenModeOtherLinkPreferenceController(Context context, String key, @@ -51,7 +76,17 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont zenMode.getId(), 0).toIntent()); preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)); - // TODO: b/346551087 - Show media icons - ((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY); + ((CircularIconsPreference) preference).displayIcons(getSoundIcons(zenMode.getPolicy())); + } + + private CircularIconSet getSoundIcons(ZenPolicy policy) { + ImmutableList.Builder icons = new ImmutableList.Builder<>(); + for (Map.Entry entry : PRIORITIES_TO_ICONS.entrySet()) { + if (policy.isCategoryAllowed(entry.getKey(), false)) { + icons.add(entry.getValue()); + } + } + return new CircularIconSet<>(icons.build(), + iconResId -> IconUtil.makeSoundIcon(mContext, iconResId)); } } diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java index 26de9eedf33..dd3a400e886 100644 --- a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java +++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java @@ -56,6 +56,8 @@ import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.notification.modes.ZenMode; +import com.google.common.collect.ImmutableList; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -85,14 +87,18 @@ class ZenModeSummaryHelper { PRIORITY_CATEGORY_REPEAT_CALLERS, }; + static final ImmutableList OTHER_SOUND_CATEGORIES = + ImmutableList.of( + PRIORITY_CATEGORY_ALARMS, + PRIORITY_CATEGORY_MEDIA, + PRIORITY_CATEGORY_SYSTEM, + PRIORITY_CATEGORY_REMINDERS, + PRIORITY_CATEGORY_EVENTS); + String getOtherSoundCategoriesSummary(ZenMode zenMode) { List enabledCategories = getEnabledCategories( zenMode.getPolicy(), - category -> PRIORITY_CATEGORY_ALARMS == category - || PRIORITY_CATEGORY_MEDIA == category - || PRIORITY_CATEGORY_SYSTEM == category - || PRIORITY_CATEGORY_REMINDERS == category - || PRIORITY_CATEGORY_EVENTS == category, + OTHER_SOUND_CATEGORIES::contains, true); int numCategories = enabledCategories.size(); MessageFormat msgFormat = new MessageFormat( diff --git a/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java b/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java index 22dc7547959..9e8524395e5 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java @@ -53,6 +53,36 @@ public class CircularIconSetTest { when(mDrawableLoader.apply(anyInt())).thenReturn(new ColorDrawable(Color.BLACK)); } + @Test + public void equals_sameItems_true() { + CircularIconSet items1 = new CircularIconSet<>(ImmutableList.of(1, 2), + num -> new ColorDrawable(Color.BLUE)); + CircularIconSet items2 = new CircularIconSet<>(ImmutableList.of(1, 2), + num -> new ColorDrawable(Color.GREEN)); + + assertThat(items1.hasSameItemsAs(items2)).isTrue(); + } + + @Test + public void equals_differentTypes_false() { + CircularIconSet items1 = new CircularIconSet<>(ImmutableList.of(1, 2), + num -> new ColorDrawable(Color.BLUE)); + CircularIconSet items2 = new CircularIconSet<>(ImmutableList.of("a", "b"), + str -> new ColorDrawable(Color.GREEN)); + + assertThat(items1.hasSameItemsAs(items2)).isFalse(); + } + + @Test + public void equals_differentItems_false() { + CircularIconSet items1 = new CircularIconSet<>(ImmutableList.of("a", "b"), + str -> new ColorDrawable(Color.GREEN)); + CircularIconSet items2 = new CircularIconSet<>(ImmutableList.of("a", "b", "c"), + str -> new ColorDrawable(Color.GREEN)); + + assertThat(items1.hasSameItemsAs(items2)).isFalse(); + } + @Test public void getIcons_loadsAllIcons() { CircularIconSet set = new CircularIconSet<>(ImmutableList.of(1, 2, 3), diff --git a/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java index 2ef62d0d29c..73754df349b 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java @@ -19,16 +19,16 @@ 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.base.Preconditions.checkState; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + 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; @@ -41,11 +41,11 @@ import com.google.common.util.concurrent.MoreExecutors; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import java.util.List; import java.util.stream.IntStream; @RunWith(RobolectricTestRunner.class) @@ -68,20 +68,30 @@ public class CircularIconsPreferenceTest { // Tests should call bindAndMeasureViewHolder() so that icons can be added. Resources res = mContext.getResources(); - mOneIconWidth = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_size) + mOneIconWidth = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); } private void bindAndMeasureViewHolder(int viewWidth) { + bindViewHolder(); + measureViewHolder(viewWidth); + } + + private void bindViewHolder() { 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); } + private void measureViewHolder(int viewWidth) { + checkState(mIconContainer != null, "Call bindViewHolder() first!"); + mIconContainer.measure(makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY), + makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); + mIconContainer.getViewTreeObserver().dispatchOnGlobalLayout(); + } + @Test public void displayIcons_loadsIcons() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), @@ -90,13 +100,10 @@ public class CircularIconsPreferenceTest { 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(mPreference.getIcons()).hasSize(2); + assertThat(((ColorDrawable) mPreference.getIcons().get(0)).getColor()).isEqualTo(1); + assertThat(((ColorDrawable) mPreference.getIcons().get(1)).getColor()).isEqualTo(2); + assertThat(mPreference.getPlusText()).isNull(); assertThat(mIconContainer.getVisibility()).isEqualTo(View.VISIBLE); } @@ -111,74 +118,81 @@ public class CircularIconsPreferenceTest { assertThat(mIconContainer.getVisibility()).isEqualTo(View.GONE); } - @Test public void displayIcons_exactlyMaxIcons_loadsAllIcons() throws Exception { int width = 300; - int fittingIcons = width / mOneIconWidth; + int fittingCircles = width / mOneIconWidth; CircularIconSet iconSet = new CircularIconSet<>( - IntStream.range(0, fittingIcons).boxed().toList(), + IntStream.range(0, fittingCircles).boxed().toList(), ColorDrawable::new); bindAndMeasureViewHolder(width); mPreference.displayIcons(iconSet); - List displayedDrawables = mPreference.getIconViews().stream() - .map(ImageView::getDrawable).toList(); - assertThat(displayedDrawables).hasSize(fittingIcons); - assertThat(displayedDrawables).containsExactlyElementsIn( + assertThat(mPreference.getIcons()).hasSize(fittingCircles); + assertThat(mPreference.getIcons()).containsExactlyElementsIn( Futures.allAsList(iconSet.getIcons()).get()).inOrder(); + assertThat(mPreference.getPlusText()).isNull(); + } @Test public void displayIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception { int width = 300; - int fittingIcons = width / mOneIconWidth; + int fittingCircles = width / mOneIconWidth; CircularIconSet iconSet = new CircularIconSet<>( - IntStream.range(0, fittingIcons + 5).boxed().toList(), + IntStream.range(0, fittingCircles + 5).boxed().toList(), ColorDrawable::new); bindAndMeasureViewHolder(width); mPreference.displayIcons(iconSet); - List 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()) + // N-1 icons, plus (+6) text. + assertThat(mPreference.getIcons()).hasSize(fittingCircles - 1); + assertThat(mPreference.getIcons()).containsExactlyElementsIn( + Futures.allAsList(iconSet.getIcons(fittingCircles - 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); + assertThat(mPreference.getPlusText()).isEqualTo("+6"); } @Test public void displayIcons_teenyTinySpace_showsPlusIcon_noCrash() { - int width = 1; CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); - bindAndMeasureViewHolder(width); + bindAndMeasureViewHolder(1); 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); + assertThat(mPreference.getIcons()).isEmpty(); + assertThat(mPreference.getPlusText()).isEqualTo("+2"); } @Test - public void displayIcons_beforeBind_loadsIconsOnBind() { + public void displayIcons_beforeBind_loadsIconsOnBindAndMeasure() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); mPreference.displayIcons(iconSet); - assertThat(mPreference.getIconViews()).isEmpty(); + assertThat(mPreference.getIcons()).isEmpty(); // Hold... - bindAndMeasureViewHolder(VIEW_WIDTH); - assertThat(mPreference.getIconViews()).hasSize(3); + bindViewHolder(); + assertThat(mPreference.getIcons()).isEmpty(); // Hooooold... + + measureViewHolder(VIEW_WIDTH); + assertThat(mPreference.getIcons()).hasSize(3); + } + + @Test + public void displayIcons_beforeMeasure_loadsIconsOnMeasure() { + CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3), + ColorDrawable::new); + bindViewHolder(); + + mPreference.displayIcons(iconSet); + assertThat(mPreference.getIcons()).isEmpty(); + + measureViewHolder(VIEW_WIDTH); + assertThat(mPreference.getIcons()).hasSize(3); } @Test @@ -192,10 +206,24 @@ public class CircularIconsPreferenceTest { bindAndMeasureViewHolder(VIEW_WIDTH); mPreference.displayIcons(threeIcons); - assertThat(mPreference.getIconViews()).hasSize(3); + assertThat(mPreference.getIcons()).hasSize(3); mPreference.displayIcons(twoIcons); - assertThat(mPreference.getIconViews()).hasSize(2); + assertThat(mPreference.getIcons()).hasSize(2); mPreference.displayIcons(fourIcons); - assertThat(mPreference.getIconViews()).hasSize(4); + assertThat(mPreference.getIcons()).hasSize(4); + } + + @Test + public void displayIcons_sameSet_doesNotReloadIcons() { + CircularIconSet one = new CircularIconSet<>(ImmutableList.of(1, 2, 3), + ColorDrawable::new); + CircularIconSet same = Mockito.spy(new CircularIconSet<>(ImmutableList.of(1, 2, 3), + ColorDrawable::new)); + when(same.getIcons()).thenThrow(new RuntimeException("Shouldn't be called!")); + + bindAndMeasureViewHolder(VIEW_WIDTH); + + mPreference.displayIcons(one); + mPreference.displayIcons(same); // if no exception, wasn't called. } } diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java index 4a6c59627bd..cc4d30643f7 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java @@ -259,7 +259,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { appEntries.add(createAppEntry("test2", mContext.getUserId())); mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getIconViews()).hasSize(2); + assertThat(mPreference.getIcons()).hasSize(2); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java index c9ea6d4ac69..7fa4f9f0e71 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java @@ -17,7 +17,7 @@ package com.android.settings.notification.modes; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -25,8 +25,10 @@ import android.app.Flags; import android.content.Context; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; import com.android.settingslib.notification.modes.TestModeBuilder; +import com.android.settingslib.notification.modes.ZenMode; import org.junit.Before; import org.junit.Rule; @@ -60,13 +62,40 @@ public final class ZenModeOtherLinkPreferenceControllerTest { } @Test - @EnableFlags(Flags.FLAG_MODES_UI) - public void testHasSummary() { + public void updateState_loadsSummary() { CircularIconsPreference pref = mock(CircularIconsPreference.class); - mController.updateZenMode(pref, TestModeBuilder.EXAMPLE); verify(pref).setSummary(any()); - verify(pref).displayIcons(eq(CircularIconSet.EMPTY)); + } + + @Test + public void updateState_loadsIcons() { + CircularIconsPreference pref = mock(CircularIconsPreference.class); + ZenMode mode = new TestModeBuilder() + .setZenPolicy(new ZenPolicy.Builder() + .disallowAllSounds() + .allowMedia(true) + .allowSystem(true) + .allowReminders(true) + .build()) + .build(); + + mController.updateState(pref, mode); + + verify(pref).displayIcons(argThat(iconSet -> iconSet.size() == 3)); + } + + @Test + public void updateState_loadsAllIcons() { + CircularIconsPreference pref = mock(CircularIconsPreference.class); + ZenMode mode = new TestModeBuilder() + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(); + + mController.updateState(pref, mode); + + verify(pref).displayIcons(argThat(iconSet -> + iconSet.size() == ZenModeSummaryHelper.OTHER_SOUND_CATEGORIES.size())); } } \ No newline at end of file