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
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 0000000..8200a9b
--- /dev/null
+++ b/res/drawable/preference_circular_icons_plus_item_background.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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.
+  -->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="oval">
+    <size
+        android:width="@dimen/zen_mode_circular_icon_diameter"
+        android:height="@dimen/zen_mode_circular_icon_diameter" />
+    <solid android:color="?androidprv:attr/materialColorSecondaryContainer" />
+    <!-- TODO: b/346551087 - Include border (or not) according to final design
+    <stroke android:width="1dp" android:color="?androidprv:attr/materialColorOnSecondaryContainer" />
+    -->
+</shape>
\ 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 3e8d7fa..e5656ce 100644
--- a/res/layout/preference_circular_icons_item.xml
+++ b/res/layout/preference_circular_icons_item.xml
@@ -17,8 +17,8 @@
 
 <ImageView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/zen_mode_circular_icon_size"
-    android:layout_height="@dimen/zen_mode_circular_icon_size"
+    android:layout_width="@dimen/zen_mode_circular_icon_diameter"
+    android:layout_height="@dimen/zen_mode_circular_icon_diameter"
     android:layout_marginTop="@dimen/zen_mode_circular_icon_margin_vertical"
     android:layout_marginBottom="@dimen/zen_mode_circular_icon_margin_vertical"
     android:layout_marginEnd="@dimen/zen_mode_circular_icon_margin_between" />
\ 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 0000000..9882086
--- /dev/null
+++ b/res/layout/preference_circular_icons_plus_item.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:layout_width="@dimen/zen_mode_circular_icon_diameter"
+    android:layout_height="@dimen/zen_mode_circular_icon_diameter"
+    android:layout_marginTop="@dimen/zen_mode_circular_icon_margin_vertical"
+    android:layout_marginBottom="@dimen/zen_mode_circular_icon_margin_vertical"
+    android:gravity="center"
+    android:padding="4dp"
+    android:drawablePadding="0dp"
+    android:background="@drawable/preference_circular_icons_plus_item_background"
+    android:textColor="?androidprv:attr/materialColorOnSecondaryContainer"
+    android:maxLines="1"
+    android:autoSizeTextType="uniform"
+    android:autoSizeMinTextSize="6sp" />
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c76fff5..2bb8fc2 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -509,7 +509,8 @@
     <dimen name="zen_mode_icon_list_item_circle_diameter">56dp</dimen>
     <dimen name="zen_mode_icon_list_item_icon_size">32dp</dimen>
     <!-- For the items in the CircularIconsPreference (contacts, apps, sound channels). -->
-    <dimen name="zen_mode_circular_icon_size">32dp</dimen>
+    <dimen name="zen_mode_circular_icon_diameter">32dp</dimen>
+    <dimen name="zen_mode_circular_icon_inner_icon_size">20dp</dimen>
     <dimen name="zen_mode_circular_icon_margin_between">4dp</dimen>
     <dimen name="zen_mode_circular_icon_margin_vertical">8dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 05fd19a..ecfc10a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -9361,6 +9361,8 @@
     <string name="zen_mode_apps_work_app"><xliff:g id="app_label" example="Chrome">%s</xliff:g> (Work)</string>
     <!-- Text displayed (for a brief time) while the list of bypassing apps is being fetched. Will be replaced by a zen_mode_apps_subtext. [CHAR_LIMIT=60] -->
     <string name="zen_mode_apps_calculating">Calculating\u2026</string>
+    <!-- Priority Modes: Format for a string displayed when there are more items (e.g. apps, contacts) that can be shown. For example, we show (A)(B)(C)(+5), where this string represents the "+5" value. Needs to be as compact as possible, since it will be drawn in a really small area. [CHAR_LIMIT=4] -->
+    <string name="zen_mode_plus_n_items">+<xliff:g id="number" example="42">%d</xliff:g></string>
 
     <!-- [CHAR LIMIT=100] Zen mode settings: Allow apps to bypass DND -->
     <string name="zen_mode_bypassing_apps">Allow apps to override</string>
diff --git a/src/com/android/settings/notification/modes/CircularIconSet.java b/src/com/android/settings/notification/modes/CircularIconSet.java
index 55a92fd..18f82d9 100644
--- a/src/com/android/settings/notification/modes/CircularIconSet.java
+++ b/src/com/android/settings/notification/modes/CircularIconSet.java
@@ -22,6 +22,7 @@
 
 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 @@
         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 1f6e0b0..5e8f720 100644
--- a/src/com/android/settings/notification/modes/CircularIconsPreference.java
+++ b/src/com/android/settings/notification/modes/CircularIconsPreference.java
@@ -21,15 +21,17 @@
 
 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 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 @@
     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<List<Drawable>> mPendingLoadIconsFuture;
 
     public CircularIconsPreference(Context context) {
         super(context);
@@ -94,17 +98,25 @@
     }
 
     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 @@
         }
         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<ListenableFuture<Drawable>> 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 @@
         } else {
             // Fit exactly or with remaining space.
             iconFutures = iconSet.getIcons();
+            extraItems = 0;
         }
 
         displayIconsWhenReady(iconFutures, extraItems);
@@ -158,33 +172,45 @@
             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 <numImages> 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 @@
                 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<? extends View> 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<ImageView> getIconViews() {
+    List<Drawable> getIcons() {
         if (mIconContainer == null) {
-            return ImmutableList.of();
+            return List.of();
         }
-        ImmutableList.Builder<ImageView> imageViews = new ImmutableList.Builder<>();
-        for (int i = 0; i < mIconContainer.getChildCount(); i++) {
-            imageViews.add((ImageView) mIconContainer.getChildAt(i));
+        ArrayList<Drawable> drawables = new ArrayList<>();
+        for (int i = 0; i < getChildCount(mIconContainer, ImageView.class); i++) {
+            drawables.add(((ImageView) mIconContainer.getChildAt(i)).getDrawable());
         }
-        return imageViews.build();
+        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;
+        }
     }
 }
diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java
index d07abf3..07e1440 100644
--- a/src/com/android/settings/notification/modes/IconUtil.java
+++ b/src/com/android/settings/notification/modes/IconUtil.java
@@ -55,7 +55,7 @@
      * 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 @@
      * 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 @@
                         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 @@
 
         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 70df9b6..1b51cfa 100644
--- a/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java
@@ -64,7 +64,7 @@
 
         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 512dabb..93df38b 100644
--- a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java
@@ -156,7 +156,7 @@
         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 452faed..fce48af 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</* @PriorityCategory */ Integer, /* @DrawableRes */ Integer>
+            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 @@
                         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<Integer> getSoundIcons(ZenPolicy policy) {
+        ImmutableList.Builder<Integer> icons = new ImmutableList.Builder<>();
+        for (Map.Entry<Integer, Integer> 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 26de9ee..dd3a400 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.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 @@
             PRIORITY_CATEGORY_REPEAT_CALLERS,
     };
 
+    static final ImmutableList</* @PriorityCategory */ Integer> 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<String> 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 22dc754..9e85243 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java
@@ -54,6 +54,36 @@
     }
 
     @Test
+    public void equals_sameItems_true() {
+        CircularIconSet<Integer> items1 = new CircularIconSet<>(ImmutableList.of(1, 2),
+                num -> new ColorDrawable(Color.BLUE));
+        CircularIconSet<Integer> items2 = new CircularIconSet<>(ImmutableList.of(1, 2),
+                num -> new ColorDrawable(Color.GREEN));
+
+        assertThat(items1.hasSameItemsAs(items2)).isTrue();
+    }
+
+    @Test
+    public void equals_differentTypes_false() {
+        CircularIconSet<Integer> items1 = new CircularIconSet<>(ImmutableList.of(1, 2),
+                num -> new ColorDrawable(Color.BLUE));
+        CircularIconSet<String> items2 = new CircularIconSet<>(ImmutableList.of("a", "b"),
+                str -> new ColorDrawable(Color.GREEN));
+
+        assertThat(items1.hasSameItemsAs(items2)).isFalse();
+    }
+
+    @Test
+    public void equals_differentItems_false() {
+        CircularIconSet<String> items1 = new CircularIconSet<>(ImmutableList.of("a", "b"),
+                str -> new ColorDrawable(Color.GREEN));
+        CircularIconSet<String> items2 = new CircularIconSet<>(ImmutableList.of("a", "b", "c"),
+                str -> new ColorDrawable(Color.GREEN));
+
+        assertThat(items1.hasSameItemsAs(items2)).isFalse();
+    }
+
+    @Test
     public void getIcons_loadsAllIcons() {
         CircularIconSet<Integer> set = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
                 mDrawableLoader);
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 2ef62d0..73754df 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 @@
 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 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 @@
         // 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<Integer> iconSet = new CircularIconSet<>(ImmutableList.of(1, 2),
@@ -90,13 +100,10 @@
         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 @@
         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<Integer> iconSet = new CircularIconSet<>(
-                IntStream.range(0, fittingIcons).boxed().toList(),
+                IntStream.range(0, fittingCircles).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(
+        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<Integer> 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<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())
+        // 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<Integer> 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<Integer> 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<Integer> 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 @@
         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<Integer> one = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
+                ColorDrawable::new);
+        CircularIconSet<Integer> 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 4a6c596..cc4d306 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 @@
         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 c9ea6d4..7fa4f9f 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.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 @@
     }
 
     @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