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
diff --git a/res/layout/preference_circular_icons.xml b/res/layout/preference_circular_icons.xml
new file mode 100644
index 0000000..ae981b2
--- /dev/null
+++ b/res/layout/preference_circular_icons.xml
@@ -0,0 +1,84 @@
+<?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.
+  -->
+
+<!-- Based off preference_two_target.xml, with the added LinearLayout for the icons. -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:gravity="center_vertical"
+    android:background="?android:attr/selectableItemBackground"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:clipToPadding="false">
+
+    <include layout="@layout/settingslib_icon_frame"/>
+
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:paddingTop="16dp"
+        android:paddingBottom="16dp">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:maxLines="2"
+            android:hyphenationFrequency="normalFast"
+            android:lineBreakWordStyle="phrase"
+            android:textAppearance="?android:attr/textAppearanceListItem"
+            android:ellipsize="marquee"/>
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/title"
+            android:layout_alignStart="@android:id/title"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="?android:attr/textColorSecondary"
+            android:hyphenationFrequency="normalFast"
+            android:lineBreakWordStyle="phrase"
+            android:maxLines="10"/>
+
+        <!-- Circular icons (32dp) will be ImageViews under this LinearLayout -->
+        <LinearLayout
+            android:id="@+id/circles_container"
+            android:orientation="horizontal"
+            android:gravity="center_vertical"
+            android:layout_width="match_parent"
+            android:layout_height="48dp"
+            android:layout_below="@android:id/summary"
+            android:layout_alignStart="@android:id/title" />
+
+    </RelativeLayout>
+
+    <include layout="@layout/preference_two_target_divider" />
+
+    <!-- Preference should place its actual preference widget here. -->
+    <LinearLayout
+        android:id="@android:id/widget_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:minWidth="@dimen/two_target_min_width"
+        android:gravity="center"
+        android:orientation="vertical" />
+
+</LinearLayout>
diff --git a/res/layout/preference_circular_icons_item.xml b/res/layout/preference_circular_icons_item.xml
new file mode 100644
index 0000000..3e8d7fa
--- /dev/null
+++ b/res/layout/preference_circular_icons_item.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+
+<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_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/values/dimens.xml b/res/values/dimens.xml
index 8a96727..c76fff5 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -508,4 +508,8 @@
     <dimen name="zen_mode_icon_list_item_size">96dp</dimen>
     <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_margin_between">4dp</dimen>
+    <dimen name="zen_mode_circular_icon_margin_vertical">8dp</dimen>
 </resources>
diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml
index 0c687b2..a8ba553 100644
--- a/res/xml/modes_rule_settings.xml
+++ b/res/xml/modes_rule_settings.xml
@@ -36,17 +36,17 @@
             android:key="allow_filtering"
             android:title="@string/mode_notification_filter_title"/>
 
-        <Preference
-                android:key="zen_mode_people"
-                android:title="@string/zen_category_people"/>
+        <com.android.settings.notification.modes.CircularIconsPreference
+            android:key="zen_mode_people"
+            android:title="@string/zen_category_people" />
 
-        <Preference
+        <com.android.settings.notification.modes.CircularIconsPreference
             android:key="zen_mode_apps"
-            android:title="@string/zen_category_apps"/>
+            android:title="@string/zen_category_apps" />
 
-        <Preference
-                android:key="zen_other_settings"
-                android:title="@string/zen_category_exceptions" />
+        <com.android.settings.notification.modes.CircularIconsPreference
+            android:key="zen_other_settings"
+            android:title="@string/zen_category_exceptions" />
     </PreferenceCategory>
 
     <!-- automatic trigger section; preference changes programmatically depending on type -->
diff --git a/src/com/android/settings/notification/modes/CircularIconSet.java b/src/com/android/settings/notification/modes/CircularIconSet.java
new file mode 100644
index 0000000..55a92fd
--- /dev/null
+++ b/src/com/android/settings/notification/modes/CircularIconSet.java
@@ -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 android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Function;
+
+/**
+ * A set of icons to be displayed in a {@link CircularIconsPreference}
+ *
+ * @param <T> The type of the items in the set. Can be an arbitrary type, the only requirement
+ *           being that the {@code drawableLoader} supplied to the constructor is able to produce
+ *           a {@link Drawable} from it (for example a resource id, a Content Uri, etc).
+ */
+class CircularIconSet<T> {
+
+    @VisibleForTesting // Can be set by tests, before creating instances.
+    static ExecutorService sExecutorService = Executors.newCachedThreadPool();
+
+    static final CircularIconSet<?> EMPTY = new CircularIconSet<>(ImmutableList.of(),
+            unused -> new ColorDrawable(Color.BLACK));
+
+    private final ImmutableList<T> mItems;
+    private final Function<T, Drawable> mDrawableLoader;
+    private final ListeningExecutorService mBackgroundExecutor;
+
+    private final ConcurrentHashMap<T, Drawable> mCachedIcons;
+
+    CircularIconSet(List<T> items, Function<T, Drawable> drawableLoader) {
+        mItems = ImmutableList.copyOf(items);
+        mDrawableLoader = drawableLoader;
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(sExecutorService);
+        mCachedIcons = new ConcurrentHashMap<>();
+    }
+
+    int size() {
+        return mItems.size();
+    }
+
+    /**
+     * Loads all icons from the set, using the supplied {@code drawableLoader}, in a background
+     * thread.
+     */
+    List<ListenableFuture<Drawable>> getIcons() {
+        return getIcons(Integer.MAX_VALUE);
+    }
+
+    /**
+     * Loads up to {@code maxSize} icons from the set, using the supplied {@code drawableLoader}, in
+     * a background thread.
+     */
+    List<ListenableFuture<Drawable>> getIcons(int maxNumber) {
+        return mItems.stream().limit(maxNumber)
+                .map(this::loadIcon)
+                .toList();
+    }
+
+    private ListenableFuture<Drawable> loadIcon(T item) {
+        return mBackgroundExecutor.submit(() -> {
+            if (mCachedIcons.containsKey(item)) {
+                return mCachedIcons.get(item);
+            }
+            Drawable drawable = mDrawableLoader.apply(item);
+            if (drawable != null) {
+                mCachedIcons.put(item, drawable);
+            }
+            return drawable;
+        });
+    }
+}
diff --git a/src/com/android/settings/notification/modes/CircularIconsPreference.java b/src/com/android/settings/notification/modes/CircularIconsPreference.java
new file mode 100644
index 0000000..1f6e0b0
--- /dev/null
+++ b/src/com/android/settings/notification/modes/CircularIconsPreference.java
@@ -0,0 +1,208 @@
+/*
+ * 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.base.Preconditions.checkNotNull;
+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.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settingslib.RestrictedPreference;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class CircularIconsPreference extends RestrictedPreference {
+
+    private Executor mUiExecutor;
+    @Nullable private LinearLayout mIconContainer;
+
+    @Nullable private CircularIconSet<?> mPendingIconSet;
+    @Nullable private ListenableFuture<?> mPendingLoadIconsFuture;
+
+    public CircularIconsPreference(Context context) {
+        super(context);
+        init(context);
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public CircularIconsPreference(Context context, Executor uiExecutor) {
+        this(context);
+        mUiExecutor = uiExecutor;
+    }
+
+    public CircularIconsPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context);
+    }
+
+    private void init(Context context) {
+        mUiExecutor = context.getMainExecutor();
+        setLayoutResource(R.layout.preference_circular_icons);
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        mIconContainer = checkNotNull((LinearLayout) holder.findViewById(R.id.circles_container));
+        displayIconsIfPending();
+    }
+
+    private void displayIconsIfPending() {
+        CircularIconSet<?> pendingIconSet = mPendingIconSet;
+        if (pendingIconSet != null) {
+            mPendingIconSet = null;
+            displayIcons(pendingIconSet);
+        }
+    }
+
+    void displayIcons(CircularIconSet<?> iconSet) {
+        if (mIconContainer == null) {
+            // Too soon, wait for bind.
+            mPendingIconSet = iconSet;
+            return;
+        }
+        mIconContainer.setVisibility(iconSet.size() != 0 ? View.VISIBLE : View.GONE);
+        if (iconSet.size() == 0) {
+            return;
+        }
+        if (mIconContainer.getMeasuredWidth() == 0) {
+            // Too soon, wait for first measure to know width.
+            mPendingIconSet = iconSet;
+            ViewTreeObserver vto = mIconContainer.getViewTreeObserver();
+            vto.addOnGlobalLayoutListener(() ->
+                    new ViewTreeObserver.OnGlobalLayoutListener() {
+                        @Override
+                        public void onGlobalLayout() {
+                            vto.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)
+                + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
+        int numIconsThatFit = availableSpace / iconHorizontalSpace;
+
+        List<ListenableFuture<Drawable>> iconFutures;
+        int extraItems = 0;
+        if (iconSet.size() > numIconsThatFit) {
+            // Reserve one space for the (+xx) circle.
+            int numIconsToShow = numIconsThatFit - 1;
+            if (numIconsToShow < 0) {
+                numIconsToShow = 0;
+            }
+            iconFutures = iconSet.getIcons(numIconsToShow);
+            extraItems = iconSet.size() - numIconsToShow;
+        } else {
+            // Fit exactly or with remaining space.
+            iconFutures = iconSet.getIcons();
+        }
+
+        displayIconsWhenReady(iconFutures, extraItems);
+    }
+
+    private void displayIconsWhenReady(List<ListenableFuture<Drawable>> iconFutures,
+            int extraItems) {
+        checkState(mIconContainer != null);
+        if (mPendingLoadIconsFuture != null) {
+            mPendingLoadIconsFuture.cancel(true);
+        }
+
+        int numCircles = iconFutures.size() + (extraItems > 0 ? 1 : 0);
+        if (mIconContainer.getChildCount() > numCircles) {
+            mIconContainer.removeViews(numCircles, mIconContainer.getChildCount() - numCircles);
+        }
+        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);
+        }
+
+        // Set up placeholders and extra items indicator.
+        for (int i = 0; i < iconFutures.size(); 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));
+        }
+        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));
+        }
+
+        // Display icons when all are ready (more consistent than randomly loading).
+        mPendingLoadIconsFuture = Futures.allAsList(iconFutures);
+        FutureUtil.whenDone(
+                Futures.allAsList(iconFutures),
+                icons -> {
+                    checkState(mIconContainer != null);
+                    for (int i = 0; i < icons.size(); i++) {
+                        ((ImageView) mIconContainer.getChildAt(i)).setImageDrawable(icons.get(i));
+                    }
+                },
+                mUiExecutor);
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    ImmutableList<ImageView> getIconViews() {
+        if (mIconContainer == null) {
+            return ImmutableList.of();
+        }
+        ImmutableList.Builder<ImageView> imageViews = new ImmutableList.Builder<>();
+        for (int i = 0; i < mIconContainer.getChildCount(); i++) {
+            imageViews.add((ImageView) mIconContainer.getChildAt(i));
+        }
+        return imageViews.build();
+    }
+}
diff --git a/src/com/android/settings/notification/modes/FutureUtil.java b/src/com/android/settings/notification/modes/FutureUtil.java
index e7bf8b9..b9a4300 100644
--- a/src/com/android/settings/notification/modes/FutureUtil.java
+++ b/src/com/android/settings/notification/modes/FutureUtil.java
@@ -18,10 +18,13 @@
 
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -42,8 +45,10 @@
             }
 
             @Override
-            public void onFailure(Throwable throwable) {
-                Log.e(TAG, String.format(errorLogMessage, errorLogMessageArgs), throwable);
+            public void onFailure(@NonNull Throwable throwable) {
+                if (!(throwable instanceof CancellationException)) {
+                    Log.e(TAG, String.format(errorLogMessage, errorLogMessageArgs), throwable);
+                }
             }
         }, executor);
     }
diff --git a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
index 9c3f267..9bff2bb 100644
--- a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
@@ -24,6 +24,7 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.service.notification.ZenPolicy;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
@@ -32,6 +33,7 @@
 import androidx.preference.Preference;
 
 import com.android.settings.R;
+import com.android.settings.Utils;
 import com.android.settings.core.SubSettingLauncher;
 import com.android.settingslib.applications.ApplicationsState;
 import com.android.settingslib.applications.ApplicationsState.AppEntry;
@@ -59,7 +61,7 @@
     private ApplicationsState.Session mAppSession;
     private final ZenHelperBackend mHelperBackend;
     private ZenMode mZenMode;
-    private Preference mPreference;
+    private CircularIconsPreference mPreference;
     private final Fragment mHost;
 
     ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
@@ -97,14 +99,21 @@
                 .setArguments(bundle)
                 .toIntent());
         mZenMode = zenMode;
-        mPreference = preference;
-        if (TextUtils.isEmpty(mPreference.getSummary())) {
-            mPreference.setSummary(R.string.zen_mode_apps_calculating);
+        mPreference = (CircularIconsPreference) preference;
+
+        if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
+            mPreference.setSummary(R.string.zen_mode_apps_none_apps);
+            mPreference.displayIcons(CircularIconSet.EMPTY);
+        } else {
+            if (TextUtils.isEmpty(mPreference.getSummary())) {
+                mPreference.setSummary(R.string.zen_mode_apps_calculating);
+            }
+            if (mApplicationsState != null && mHost != null) {
+                mAppSession = mApplicationsState.newSession(mAppSessionCallbacks,
+                        mHost.getLifecycle());
+            }
+            triggerUpdateAppsBypassingDnd();
         }
-        if (mApplicationsState != null && mHost != null) {
-            mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, mHost.getLifecycle());
-        }
-        triggerUpdateAppsBypassingDnd();
     }
 
     private void triggerUpdateAppsBypassingDnd() {
@@ -126,6 +135,9 @@
         ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);
 
         mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));
+
+        mPreference.displayIcons(new CircularIconSet<>(apps,
+                app -> Utils.getBadgedIcon(mContext, app.info)));
     }
 
     @VisibleForTesting
diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
index 248ef1d..452faed 100644
--- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
@@ -17,15 +17,12 @@
 package com.android.settings.notification.modes;
 
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
-import android.os.Bundle;
 
 import androidx.annotation.NonNull;
 import androidx.preference.Preference;
 
-import com.android.settings.core.SubSettingLauncher;
 import com.android.settingslib.notification.modes.ZenMode;
 
 /**
@@ -48,13 +45,13 @@
 
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
-        Bundle bundle = new Bundle();
-        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
-        preference.setIntent(new SubSettingLauncher(mContext)
-                .setDestination(ZenModeOtherFragment.class.getName())
-                .setSourceMetricsCategory(0)
-                .setArguments(bundle)
-                .toIntent());
+        // TODO: b/332937635 - Update metrics category
+        preference.setIntent(
+                ZenSubSettingLauncher.forModeFragment(mContext, ZenModeOtherFragment.class,
+                        zenMode.getId(), 0).toIntent());
+
         preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode));
+        // TODO: b/346551087 - Show media icons
+        ((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY);
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
index 936cea6..2a61418 100644
--- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
@@ -17,15 +17,12 @@
 package com.android.settings.notification.modes;
 
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
-import android.os.Bundle;
 
 import androidx.annotation.NonNull;
 import androidx.preference.Preference;
 
-import com.android.settings.core.SubSettingLauncher;
 import com.android.settingslib.notification.modes.ZenMode;
 
 /**
@@ -48,14 +45,13 @@
 
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
-        Bundle bundle = new Bundle();
-        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
-        preference.setIntent(new SubSettingLauncher(mContext)
-                .setDestination(ZenModePeopleFragment.class.getName())
-                .setSourceMetricsCategory(0)
-                .setArguments(bundle)
-                .toIntent());
+        preference.setIntent(
+                ZenSubSettingLauncher.forModeFragment(mContext, ZenModePeopleFragment.class,
+                        zenMode.getId(), 0).toIntent());
+
         preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode));
+        // TODO: b/346551087 - Show people icons
+        ((CircularIconsPreference) preference).displayIcons(CircularIconSet.EMPTY);
     }
 }
diff --git a/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java b/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java
new file mode 100644
index 0000000..22dc754
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/CircularIconSetTest.java
@@ -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);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java
new file mode 100644
index 0000000..2ef62d0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java
@@ -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);
+    }
+}
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 6d12594..4a6c596 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
@@ -23,10 +23,13 @@
 
 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.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.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 @@
 public final class ZenModeAppsLinkPreferenceControllerTest {
 
     private ZenModeAppsLinkPreferenceController mController;
+    private CircularIconsPreference mPreference;
 
     private Context mContext;
     @Mock
@@ -91,10 +100,21 @@
     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 @@
 
     @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 @@
     }
 
     @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 @@
 
         // 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 @@
 
     @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();
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 772bd1d..c9ea6d4 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java
@@ -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.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 @@
     @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));
     }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java
index dd97d6e..0db26c3 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java
@@ -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.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 @@
     @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));
     }
 }
\ No newline at end of file