Merge "Making rebootPromptAndWipeUserData a SystemApi" into main
diff --git a/core/res/res/drawable/ic_zen_mode_type_special_dnd.xml b/core/res/res/drawable/ic_zen_mode_type_special_dnd.xml
new file mode 100644
index 0000000..fee9b0e
--- /dev/null
+++ b/core/res/res/drawable/ic_zen_mode_type_special_dnd.xml
@@ -0,0 +1,25 @@
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?android:attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M280,520L680,520L680,440L280,440L280,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
+</vector>
\ No newline at end of file
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index cf0fc61..bbe661e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5575,6 +5575,7 @@
   <java-symbol type="drawable" name="ic_zen_mode_type_schedule_time" />
   <java-symbol type="drawable" name="ic_zen_mode_type_theater" />
   <java-symbol type="drawable" name="ic_zen_mode_type_unknown" />
+  <java-symbol type="drawable" name="ic_zen_mode_type_special_dnd" />
 
   <!-- System notification for background user sound -->
   <java-symbol type="string" name="bg_user_sound_notification_title_alarm" />
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 029e6f4..25fae76 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -4567,8 +4567,8 @@
      *     when the request is flagged with {@link #AUDIOFOCUS_FLAG_DELAY_OK}.
      * @param requestAttributes non null {@link AudioAttributes} describing the main reason for
      *     requesting audio focus.
-     * @param durationHint use {@link #AUDIOFOCUS_GAIN_TRANSIENT} to indicate this focus request
-     *      is temporary, and focus will be abandonned shortly. Examples of transient requests are
+     * @param focusReqType use {@link #AUDIOFOCUS_GAIN_TRANSIENT} to indicate this focus request
+     *      is temporary, and focus will be abandoned shortly. Examples of transient requests are
      *      for the playback of driving directions, or notifications sounds.
      *      Use {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} to indicate also that it's ok for
      *      the previous focus owner to keep playing if it ducks its audio output.
@@ -4593,13 +4593,13 @@
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
     public int requestAudioFocus(OnAudioFocusChangeListener l,
             @NonNull AudioAttributes requestAttributes,
-            int durationHint,
+            int focusReqType,
             int flags) throws IllegalArgumentException {
         if (flags != (flags & AUDIOFOCUS_FLAGS_APPS)) {
             throw new IllegalArgumentException("Invalid flags 0x"
                     + Integer.toHexString(flags).toUpperCase());
         }
-        return requestAudioFocus(l, requestAttributes, durationHint,
+        return requestAudioFocus(l, requestAttributes, focusReqType,
                 flags & AUDIOFOCUS_FLAGS_APPS,
                 null /* no AudioPolicy*/);
     }
@@ -4614,7 +4614,7 @@
      *     {@link #requestAudioFocus(OnAudioFocusChangeListener, AudioAttributes, int, int)}
      * @param requestAttributes non null {@link AudioAttributes} describing the main reason for
      *     requesting audio focus.
-     * @param durationHint see the description of the same parameter in
+     * @param focusReqType see the description of the same parameter in
      *     {@link #requestAudioFocus(OnAudioFocusChangeListener, AudioAttributes, int, int)}
      * @param flags 0 or a combination of {link #AUDIOFOCUS_FLAG_DELAY_OK},
      *     {@link #AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS}, and {@link #AUDIOFOCUS_FLAG_LOCK}.
@@ -4636,14 +4636,14 @@
     })
     public int requestAudioFocus(OnAudioFocusChangeListener l,
             @NonNull AudioAttributes requestAttributes,
-            int durationHint,
+            int focusReqType,
             int flags,
             AudioPolicy ap) throws IllegalArgumentException {
         // parameter checking
         if (requestAttributes == null) {
             throw new IllegalArgumentException("Illegal null AudioAttributes argument");
         }
-        if (!AudioFocusRequest.isValidFocusGain(durationHint)) {
+        if (!AudioFocusRequest.isValidFocusGain(focusReqType)) {
             throw new IllegalArgumentException("Invalid duration hint");
         }
         if (flags != (flags & AUDIOFOCUS_FLAGS_SYSTEM)) {
@@ -4664,7 +4664,7 @@
                     "Illegal null audio policy when locking audio focus");
         }
 
-        final AudioFocusRequest afr = new AudioFocusRequest.Builder(durationHint)
+        final AudioFocusRequest afr = new AudioFocusRequest.Builder(focusReqType)
                 .setOnAudioFocusChangeListenerInt(l, null /* no Handler for this legacy API */)
                 .setAudioAttributes(requestAttributes)
                 .setAcceptsDelayedFocusGain((flags & AUDIOFOCUS_FLAG_DELAY_OK)
@@ -5025,16 +5025,16 @@
      * to identify this use case.
      * @param streamType use STREAM_RING for focus requests when ringing, VOICE_CALL for
      *    the establishment of the call
-     * @param durationHint the type of focus request. AUDIOFOCUS_GAIN_TRANSIENT is recommended so
+     * @param focusReqType the type of focus request. AUDIOFOCUS_GAIN_TRANSIENT is recommended so
      *    media applications resume after a call
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public void requestAudioFocusForCall(int streamType, int durationHint) {
+    public void requestAudioFocusForCall(int streamType, int focusReqType) {
         final IAudioService service = getService();
         try {
             service.requestAudioFocus(new AudioAttributes.Builder()
                         .setInternalLegacyStreamType(streamType).build(),
-                    durationHint, mICallBack, null,
+                    focusReqType, mICallBack, null,
                     AudioSystem.IN_VOICE_COMM_FOCUS_ID,
                     getContext().getOpPackageName(),
                     getContext().getAttributionTag(),
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index e0c3461..a96562d 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -252,7 +252,7 @@
 
     boolean isBluetoothA2dpOn();
 
-    int requestAudioFocus(in AudioAttributes aa, int durationHint, IBinder cb,
+    int requestAudioFocus(in AudioAttributes aa, int focusReqType, IBinder cb,
             IAudioFocusDispatcher fd, in String clientId, in String callingPackageName,
             in String attributionTag, int flags, IAudioPolicyCallback pcb, int sdk);
 
@@ -562,7 +562,7 @@
 
     long getMaxAdditionalOutputDeviceDelay(in AudioDeviceAttributes device);
 
-    int requestAudioFocusForTest(in AudioAttributes aa, int durationHint, IBinder cb,
+    int requestAudioFocusForTest(in AudioAttributes aa, int focusReqType, IBinder cb,
             in IAudioFocusDispatcher fd, in String clientId, in String callingPackageName,
             int flags, int uid, int sdk);
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIcon.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIcon.java
new file mode 100644
index 0000000..001701e
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIcon.java
@@ -0,0 +1,48 @@
+/*
+ * 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.settingslib.notification.modes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Icon of a Zen Mode, already loaded from the owner's resources (if specified) or from a default.
+ */
+public record ZenIcon(@NonNull Key key, @NonNull Drawable drawable) {
+
+    /**
+     * Key of a Zen Mode Icon.
+     *
+     * <p>{@link #resPackage()} will be null if the resource belongs to the system, and thus can
+     * be loaded with any {@code Context}.
+     */
+    public record Key(@Nullable String resPackage, @DrawableRes int resId) {
+
+        public Key {
+            checkArgument(resId != 0, "Resource id must be valid");
+        }
+
+        static Key forSystemResource(@DrawableRes int resId) {
+            return new Key(null, resId);
+        }
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconKeys.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconKeys.java
new file mode 100644
index 0000000..0a0b65b
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconKeys.java
@@ -0,0 +1,72 @@
+/*
+ * 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.settingslib.notification.modes;
+
+import android.app.AutomaticZenRule;
+
+import com.android.internal.R;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Known icon keys for zen modes that lack a custom {@link AutomaticZenRule#getIconResId()}, based
+ * on their {@link ZenMode.Kind} and {@link ZenMode#getType}.
+ */
+class ZenIconKeys {
+
+    /** The icon for Do Not Disturb mode. */
+    static final ZenIcon.Key MANUAL_DND = ZenIcon.Key.forSystemResource(
+            R.drawable.ic_zen_mode_type_special_dnd);
+
+    /**
+     * The default icon for implicit modes (they can also have a specific icon, if the user has
+     * chosen one via Settings).
+     */
+    static final ZenIcon.Key IMPLICIT_MODE_DEFAULT = ZenIcon.Key.forSystemResource(
+            R.drawable.ic_zen_mode_type_unknown);
+
+    private static final ImmutableMap<Integer, ZenIcon.Key> TYPE_DEFAULTS = ImmutableMap.of(
+            AutomaticZenRule.TYPE_UNKNOWN,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_unknown),
+            AutomaticZenRule.TYPE_OTHER,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_other),
+            AutomaticZenRule.TYPE_SCHEDULE_TIME,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_schedule_time),
+            AutomaticZenRule.TYPE_SCHEDULE_CALENDAR,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_schedule_calendar),
+            AutomaticZenRule.TYPE_BEDTIME,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_bedtime),
+            AutomaticZenRule.TYPE_DRIVING,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_driving),
+            AutomaticZenRule.TYPE_IMMERSIVE,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_immersive),
+            AutomaticZenRule.TYPE_THEATER,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_theater),
+            AutomaticZenRule.TYPE_MANAGED,
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_managed)
+    );
+
+    private static final ZenIcon.Key FOR_UNEXPECTED_TYPE =
+            ZenIcon.Key.forSystemResource(R.drawable.ic_zen_mode_type_unknown);
+
+    /** Default icon descriptors per mode {@link AutomaticZenRule.Type}. */
+    static ZenIcon.Key forType(@AutomaticZenRule.Type int ruleType) {
+        return TYPE_DEFAULTS.getOrDefault(ruleType, FOR_UNEXPECTED_TYPE);
+    }
+
+    private ZenIconKeys() { }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
index 271d5c4..fe0f98a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
@@ -16,28 +16,24 @@
 
 package com.android.settingslib.notification.modes;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.DrawableRes;
-import android.annotation.Nullable;
-import android.app.AutomaticZenRule;
 import android.content.Context;
 import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.InsetDrawable;
-import android.service.notification.SystemZenRules;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.LruCache;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.google.common.util.concurrent.FluentFuture;
-import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
@@ -54,7 +50,7 @@
     @Nullable // Until first usage
     private static ZenIconLoader sInstance;
 
-    private final LruCache<String, Drawable> mCache;
+    private final LruCache<ZenIcon.Key, Drawable> mCache;
     private final ListeningExecutorService mBackgroundExecutor;
 
     public static ZenIconLoader getInstance() {
@@ -64,90 +60,85 @@
         return sInstance;
     }
 
+    /** Replaces the singleton instance of {@link ZenIconLoader} by the provided one. */
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public static void setInstance(@Nullable ZenIconLoader instance) {
+        sInstance = instance;
+    }
+
     private ZenIconLoader() {
         this(Executors.newFixedThreadPool(4));
     }
 
     @VisibleForTesting
-    ZenIconLoader(ExecutorService backgroundExecutor) {
+    public ZenIconLoader(ExecutorService backgroundExecutor) {
         mCache = new LruCache<>(50);
         mBackgroundExecutor =
                 MoreExecutors.listeningDecorator(backgroundExecutor);
     }
 
+    /**
+     * Loads the {@link Drawable} corresponding to a {@link ZenMode} in a background thread, and
+     * caches it for future calls.
+     *
+     * <p>The {@link ZenIcon#drawable()} will always correspond to the resource indicated by
+     * {@link ZenIcon#key()}. In turn, this will match the value of {@link ZenMode#getIconKey()}
+     * for the supplied mode -- except for the rare case where the mode has an apparently valid
+     * drawable resource id that we fail to load for some reason, thus needing a "fallback" icon.
+     */
     @NonNull
-    ListenableFuture<Drawable> getIcon(Context context, @NonNull AutomaticZenRule rule) {
-        if (rule.getIconResId() == 0) {
-            return Futures.immediateFuture(getFallbackIcon(context, rule.getType()));
-        }
+    public ListenableFuture<ZenIcon> getIcon(@NonNull Context context, @NonNull ZenMode mode) {
+        ZenIcon.Key key = mode.getIconKey();
 
-        return FluentFuture.from(loadIcon(context, rule.getPackageName(), rule.getIconResId()))
-                .transform(icon ->
-                                icon != null ? icon : getFallbackIcon(context, rule.getType()),
-                        MoreExecutors.directExecutor());
+        return FluentFuture.from(loadIcon(context, key, /* useMonochromeIfPresent= */ true))
+                .transformAsync(drawable ->
+                        drawable != null
+                            ? immediateFuture(new ZenIcon(key, drawable))
+                            : getFallbackIcon(context, mode),
+                mBackgroundExecutor);
+    }
+
+    private ListenableFuture<ZenIcon> getFallbackIcon(Context context, ZenMode mode) {
+        ZenIcon.Key key = ZenIconKeys.forType(mode.getType());
+        return FluentFuture.from(loadIcon(context, key, /* useMonochromeIfPresent= */ false))
+                .transform(drawable -> {
+                    checkNotNull(drawable, "Couldn't load DEFAULT icon for mode %s!", mode);
+                    return new ZenIcon(key, drawable);
+                },
+                directExecutor());
     }
 
     @NonNull
-    private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context, String pkg,
-            int iconResId) {
-        String cacheKey = pkg + ":" + iconResId;
+    private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context,
+            ZenIcon.Key key, boolean useMonochromeIfPresent) {
         synchronized (mCache) {
-            Drawable cachedValue = mCache.get(cacheKey);
+            Drawable cachedValue = mCache.get(key);
             if (cachedValue != null) {
                 return immediateFuture(cachedValue != MISSING ? cachedValue : null);
             }
         }
 
         return FluentFuture.from(mBackgroundExecutor.submit(() -> {
-            if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
-                return context.getDrawable(iconResId);
+            if (TextUtils.isEmpty(key.resPackage())) {
+                return context.getDrawable(key.resId());
             } else {
-                Context appContext = context.createPackageContext(pkg, 0);
-                Drawable appDrawable = appContext.getDrawable(iconResId);
-                return getMonochromeIconIfPresent(appDrawable);
+                Context appContext = context.createPackageContext(key.resPackage(), 0);
+                Drawable appDrawable = appContext.getDrawable(key.resId());
+                return useMonochromeIfPresent
+                        ? getMonochromeIconIfPresent(appDrawable)
+                        : appDrawable;
             }
         })).catching(Exception.class, ex -> {
             // If we cannot resolve the icon, then store MISSING in the cache below, so
             // we don't try again.
-            Log.e(TAG, "Error while loading icon " + cacheKey, ex);
+            Log.e(TAG, "Error while loading mode icon " + key, ex);
             return null;
-        }, MoreExecutors.directExecutor()).transform(drawable -> {
+        }, directExecutor()).transform(drawable -> {
             synchronized (mCache) {
-                mCache.put(cacheKey, drawable != null ? drawable : MISSING);
+                mCache.put(key, drawable != null ? drawable : MISSING);
             }
             return drawable;
-        }, MoreExecutors.directExecutor());
-    }
-
-    private static Drawable getFallbackIcon(Context context, int ruleType) {
-        int iconResIdFromType = getIconResourceIdFromType(ruleType);
-        return requireNonNull(context.getDrawable(iconResIdFromType));
-    }
-
-    /** Return the default icon resource associated to a {@link AutomaticZenRule.Type} */
-    @DrawableRes
-    public static int getIconResourceIdFromType(@AutomaticZenRule.Type int ruleType) {
-        return switch (ruleType) {
-            case AutomaticZenRule.TYPE_UNKNOWN ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_unknown;
-            case AutomaticZenRule.TYPE_OTHER ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_other;
-            case AutomaticZenRule.TYPE_SCHEDULE_TIME ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_schedule_time;
-            case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar;
-            case AutomaticZenRule.TYPE_BEDTIME ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_bedtime;
-            case AutomaticZenRule.TYPE_DRIVING ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_driving;
-            case AutomaticZenRule.TYPE_IMMERSIVE ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_immersive;
-            case AutomaticZenRule.TYPE_THEATER ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_theater;
-            case AutomaticZenRule.TYPE_MANAGED ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_managed;
-            default -> com.android.internal.R.drawable.ic_zen_mode_type_unknown;
-        };
+        }, directExecutor());
     }
 
     private static Drawable getMonochromeIconIfPresent(Drawable icon) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
index 9fa8fc3..36975c7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -20,9 +20,9 @@
 import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.service.notification.SystemZenRules.PACKAGE_ANDROID;
 import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
 import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
-import static android.service.notification.SystemZenRules.PACKAGE_ANDROID;
 import static android.service.notification.ZenModeConfig.tryParseCountdownConditionId;
 import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
 import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;
@@ -36,7 +36,6 @@
 import android.app.AutomaticZenRule;
 import android.app.NotificationManager;
 import android.content.Context;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -50,12 +49,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.settingslib.R;
-
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.Comparator;
 import java.util.Objects;
@@ -275,28 +270,32 @@
     }
 
     /**
-     * Returns an icon "key" that is guaranteed to be different if the icon is different. Note that
-     * the inverse is not true, i.e. two keys can be different and the icon still be visually the
-     * same.
+     * Returns the {@link ZenIcon.Key} corresponding to the icon resource for this mode. This can be
+     * either app-provided (via {@link AutomaticZenRule#setIconResId}, user-chosen (via the icon
+     * picker in Settings), or a default icon based on the mode {@link Kind} and {@link #getType}.
      */
     @NonNull
-    public String getIconKey() {
-        return mRule.getType() + ":" + mRule.getPackageName() + ":" + mRule.getIconResId();
-    }
-
-    /**
-     * Returns the mode icon -- which can be either app-provided (via {@code addAutomaticZenRule}),
-     * user-chosen (via the icon picker in Settings), or a default icon based on the mode type.
-     */
-    @NonNull
-    public ListenableFuture<Drawable> getIcon(@NonNull Context context,
-            @NonNull ZenIconLoader iconLoader) {
-        if (mKind == Kind.MANUAL_DND) {
-            return Futures.immediateFuture(requireNonNull(
-                    context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
+    public ZenIcon.Key getIconKey() {
+        if (isManualDnd()) {
+            return ZenIconKeys.MANUAL_DND;
         }
-
-        return iconLoader.getIcon(context, mRule);
+        if (mRule.getIconResId() != 0) {
+            if (isSystemOwned()) {
+                // System-owned rules can only have system icons.
+                return ZenIcon.Key.forSystemResource(mRule.getIconResId());
+            } else {
+                // Technically, the icon of an app-provided rule could be a system icon if the
+                // user chose one with the picker. However, we cannot know for sure.
+                return new ZenIcon.Key(mRule.getPackageName(), mRule.getIconResId());
+            }
+        } else {
+            // Using a default icon (which is always a system icon).
+            if (mKind == Kind.IMPLICIT) {
+                return ZenIconKeys.IMPLICIT_MODE_DEFAULT;
+            } else {
+                return ZenIconKeys.forType(getType());
+            }
+        }
     }
 
     @NonNull
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
index 20461e3..6eb5f5b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
@@ -16,17 +16,12 @@
 
 package com.android.settingslib.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.AutomaticZenRule;
 import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.service.notification.ZenPolicy;
+import android.service.notification.SystemZenRules;
 
-import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.Before;
@@ -48,44 +43,73 @@
     }
 
     @Test
-    public void getIcon_systemOwnedRuleWithIcon_loads() throws Exception {
-        AutomaticZenRule systemRule = newRuleBuilder()
-                .setPackage("android")
+    public void getIcon_systemOwnedModeWithIcon_loads() throws Exception {
+        ZenMode mode = new TestModeBuilder()
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
                 .setIconResId(android.R.drawable.ic_media_play)
                 .build();
 
-        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, systemRule);
-        assertThat(loadFuture.isDone()).isTrue();
-        assertThat(loadFuture.get()).isNotNull();
+        ZenIcon icon = mLoader.getIcon(mContext, mode).get();
+
+        assertThat(icon.drawable()).isNotNull();
+        assertThat(icon.key().resPackage()).isNull();
+        assertThat(icon.key().resId()).isEqualTo(android.R.drawable.ic_media_play);
     }
 
     @Test
-    public void getIcon_ruleWithoutSpecificIcon_loadsFallback() throws Exception {
-        AutomaticZenRule rule = newRuleBuilder()
+    public void getIcon_modeWithoutSpecificIcon_loadsFallback() throws Exception {
+        ZenMode mode = new TestModeBuilder()
                 .setType(AutomaticZenRule.TYPE_DRIVING)
                 .setPackage("com.blah")
                 .build();
 
-        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
-        assertThat(loadFuture.isDone()).isTrue();
-        assertThat(loadFuture.get()).isNotNull();
+        ZenIcon icon = mLoader.getIcon(mContext, mode).get();
+
+        assertThat(icon.drawable()).isNotNull();
+        assertThat(icon.key().resPackage()).isNull();
+        assertThat(icon.key().resId()).isEqualTo(
+                com.android.internal.R.drawable.ic_zen_mode_type_driving);
     }
 
     @Test
     public void getIcon_ruleWithAppIconWithLoadFailure_loadsFallback() throws Exception {
-        AutomaticZenRule rule = newRuleBuilder()
+        ZenMode mode = new TestModeBuilder()
                 .setType(AutomaticZenRule.TYPE_DRIVING)
                 .setPackage("com.blah")
                 .setIconResId(-123456)
                 .build();
 
-        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
-        assertThat(loadFuture.get()).isNotNull();
+        ZenIcon icon = mLoader.getIcon(mContext, mode).get();
+
+        assertThat(icon.drawable()).isNotNull();
+        assertThat(icon.key().resPackage()).isNull();
+        assertThat(icon.key().resId()).isEqualTo(
+                com.android.internal.R.drawable.ic_zen_mode_type_driving);
     }
 
-    private static AutomaticZenRule.Builder newRuleBuilder() {
-        return new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().build());
+    @Test
+    public void getIcon_cachesCustomIcons() throws Exception {
+        ZenMode mode = new TestModeBuilder()
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .setIconResId(android.R.drawable.ic_media_play)
+                .build();
+
+        ZenIcon iconOne = mLoader.getIcon(mContext, mode).get();
+        ZenIcon iconTwo = mLoader.getIcon(mContext, mode).get();
+
+        assertThat(iconOne.drawable()).isSameInstanceAs(iconTwo.drawable());
+    }
+
+    @Test
+    public void getIcon_cachesDefaultIcons() throws Exception {
+        ZenMode mode = new TestModeBuilder()
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .setType(AutomaticZenRule.TYPE_IMMERSIVE)
+                .build();
+
+        ZenIcon iconOne = mLoader.getIcon(mContext, mode).get();
+        ZenIcon iconTwo = mLoader.getIcon(mContext, mode).get();
+
+        assertThat(iconOne.drawable()).isSameInstanceAs(iconTwo.drawable());
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
index 32216fa..e64b0c6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -20,6 +20,7 @@
 import static android.app.AutomaticZenRule.TYPE_DRIVING;
 import static android.app.AutomaticZenRule.TYPE_IMMERSIVE;
 import static android.app.AutomaticZenRule.TYPE_OTHER;
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
 import static android.app.AutomaticZenRule.TYPE_THEATER;
 import static android.app.AutomaticZenRule.TYPE_UNKNOWN;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
@@ -30,14 +31,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
 import android.app.AutomaticZenRule;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Parcel;
 import android.service.notification.Condition;
@@ -47,12 +41,9 @@
 
 import com.android.internal.R;
 
-import com.google.common.util.concurrent.ListenableFuture;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -289,37 +280,97 @@
     }
 
     @Test
-    public void getIcon_normalMode_loadsIconNormally() {
-        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
-        ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false));
+    public void getIconKey_normalModeWithCustomIcon_isCustomIcon() {
+        ZenMode mode = new TestModeBuilder()
+                .setType(TYPE_BEDTIME)
+                .setPackage("some.package")
+                .setIconResId(123)
+                .build();
 
-        ListenableFuture<Drawable> unused = mode.getIcon(RuntimeEnvironment.getApplication(),
-                iconLoader);
+        ZenIcon.Key iconKey = mode.getIconKey();
 
-        verify(iconLoader).getIcon(any(), eq(ZEN_RULE));
+        assertThat(iconKey.resPackage()).isEqualTo("some.package");
+        assertThat(iconKey.resId()).isEqualTo(123);
     }
 
     @Test
-    public void getIcon_manualDnd_returnsFixedIcon() {
-        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+    public void getIconKey_systemOwnedModeWithCustomIcon_isCustomIcon() {
+        ZenMode mode = new TestModeBuilder()
+                .setType(TYPE_SCHEDULE_CALENDAR)
+                .setPackage(PACKAGE_ANDROID)
+                .setIconResId(123)
+                .build();
 
-        ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getIcon(
-                RuntimeEnvironment.getApplication(), iconLoader);
+        ZenIcon.Key iconKey = mode.getIconKey();
 
-        assertThat(future.isDone()).isTrue();
-        verify(iconLoader, never()).getIcon(any(), any());
+        assertThat(iconKey.resPackage()).isNull();
+        assertThat(iconKey.resId()).isEqualTo(123);
     }
 
     @Test
-    public void getIcon_implicitMode_loadsIconNormally() {
-        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
-        ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
-                zenConfigRuleFor(IMPLICIT_ZEN_RULE, false));
+    public void getIconKey_implicitModeWithCustomIcon_isCustomIcon() {
+        ZenMode mode = new TestModeBuilder()
+                .setId(ZenModeConfig.implicitRuleId("some.package"))
+                .setPackage("some.package")
+                .setIconResId(123)
+                .build();
 
-        ListenableFuture<Drawable> unused = mode.getIcon(RuntimeEnvironment.getApplication(),
-                iconLoader);
+        ZenIcon.Key iconKey = mode.getIconKey();
 
-        verify(iconLoader).getIcon(any(), eq(IMPLICIT_ZEN_RULE));
+        assertThat(iconKey.resPackage()).isEqualTo("some.package");
+        assertThat(iconKey.resId()).isEqualTo(123);
+    }
+
+    @Test
+    public void getIconKey_manualDnd_isDndIcon() {
+        ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND_INACTIVE.getIconKey();
+
+        assertThat(iconKey.resPackage()).isNull();
+        assertThat(iconKey.resId()).isEqualTo(
+                com.android.internal.R.drawable.ic_zen_mode_type_special_dnd);
+    }
+
+    @Test
+    public void getIconKey_normalModeWithoutCustomIcon_isModeTypeIcon() {
+        ZenMode mode = new TestModeBuilder()
+                .setType(TYPE_BEDTIME)
+                .setPackage("some.package")
+                .build();
+
+        ZenIcon.Key iconKey = mode.getIconKey();
+
+        assertThat(iconKey.resPackage()).isNull();
+        assertThat(iconKey.resId()).isEqualTo(
+                com.android.internal.R.drawable.ic_zen_mode_type_bedtime);
+    }
+
+    @Test
+    public void getIconKey_systemOwnedModeWithoutCustomIcon_isModeTypeIcon() {
+        ZenMode mode = new TestModeBuilder()
+                .setType(TYPE_SCHEDULE_CALENDAR)
+                .setPackage(PACKAGE_ANDROID)
+                .build();
+
+        ZenIcon.Key iconKey = mode.getIconKey();
+
+        assertThat(iconKey.resPackage()).isNull();
+        assertThat(iconKey.resId()).isEqualTo(
+                com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar);
+    }
+
+    @Test
+    public void getIconKey_implicitModeWithoutCustomIcon_isSpecialIcon() {
+        ZenMode mode = new TestModeBuilder()
+                .setId(ZenModeConfig.implicitRuleId("some.package"))
+                .setPackage("some_package")
+                .setType(TYPE_BEDTIME) // Type should be ignored.
+                .build();
+
+        ZenIcon.Key iconKey = mode.getIconKey();
+
+        assertThat(iconKey.resPackage()).isNull();
+        assertThat(iconKey.resId()).isEqualTo(
+                com.android.internal.R.drawable.ic_zen_mode_type_unknown);
     }
 
     private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
index e0a53f8..5925819 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
@@ -24,6 +24,7 @@
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.modes.ZenIconLoader
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.asIcon
 import com.android.systemui.coroutines.collectValues
@@ -35,6 +36,7 @@
 import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.toCollection
@@ -61,6 +63,7 @@
             addOverride(com.android.internal.R.drawable.ic_zen_mode_type_bedtime, BEDTIME_DRAWABLE)
             addOverride(com.android.internal.R.drawable.ic_zen_mode_type_driving, DRIVING_DRAWABLE)
         }
+        ZenIconLoader.setInstance(ZenIconLoader(MoreExecutors.newDirectExecutorService()))
     }
 
     @EnableFlags(Flags.FLAG_MODES_UI)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
index d2bc54e0..33f379d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
@@ -23,6 +23,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.ZenIconLoader
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -34,10 +35,12 @@
 import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogEventLogger
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.clearInvocations
@@ -64,6 +67,11 @@
             mockDialogEventLogger
         )
 
+    @Before
+    fun setUp() {
+        ZenIconLoader.setInstance(ZenIconLoader(MoreExecutors.newDirectExecutorService()))
+    }
+
     @Test
     fun tiles_filtersOutUserDisabledModes() =
         testScope.runTest {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index d1b5160..05bd1a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -44,7 +44,6 @@
 
 import androidx.lifecycle.Observer;
 
-import com.android.settingslib.notification.modes.ZenIconLoader;
 import com.android.settingslib.notification.modes.ZenMode;
 import com.android.systemui.Flags;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -404,13 +403,9 @@
         boolean visible = mode != null;
         if (visible) {
             // TODO: b/360399800 - Get the resource id, package, and cached drawable from the mode;
-            //  this is a shortcut for testing (there should be no direct dependency on
-            //  ZenIconLoader here).
-            String resPackage = mode.isSystemOwned() ? null : mode.getRule().getPackageName();
-            int iconResId = mode.getRule().getIconResId();
-            if (iconResId == 0) {
-                iconResId = ZenIconLoader.getIconResourceIdFromType(mode.getType());
-            }
+            //  this is a shortcut for testing.
+            String resPackage = mode.getIconKey().resPackage();
+            int iconResId = mode.getIconKey().resId();
 
             mIconController.setResourceIcon(mSlotZen, resPackage, iconResId,
                     /* preloadedIcon= */ null, mode.getName());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index d351da6..a67b47a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -92,7 +92,7 @@
     }
 
     suspend fun getModeIcon(mode: ZenMode): Icon {
-        return mode.getIcon(context, iconLoader).await().asIcon()
+        return iconLoader.getIcon(context, mode).await().drawable().asIcon()
     }
 
     /**
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java
index 95559802..7f79556 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java
@@ -89,7 +89,7 @@
 
     @Override
     protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
-        cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags);
+        cancelPendingTransitions();
         if (!isInsideSlop(rawEvent, mTouchSlop)) {
             cancelGesture(event, rawEvent, policyFlags);
         }
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java
index 15e1278..872ade5 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java
@@ -46,7 +46,6 @@
     @Override
     protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
         super.onUp(event, rawEvent, policyFlags);
-        cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 14dca4e..b738482 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -10730,7 +10730,7 @@
         return true;
     }
 
-    public int requestAudioFocus(AudioAttributes aa, int durationHint, IBinder cb,
+    public int requestAudioFocus(AudioAttributes aa, int focusReqType, IBinder cb,
             IAudioFocusDispatcher fd, String clientId, String callingPackageName,
             String attributionTag, int flags, IAudioPolicyCallback pcb, int sdk) {
         if ((flags & AudioManager.AUDIOFOCUS_FLAG_TEST) != 0) {
@@ -10739,7 +10739,7 @@
         final int uid = Binder.getCallingUid();
         MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "focus")
                 .setUid(uid)
-                //.putInt("durationHint", durationHint)
+                //.putInt("focusReqType", focusReqType)
                 .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName)
                 .set(MediaMetrics.Property.CLIENT_NAME, clientId)
                 .set(MediaMetrics.Property.EVENT, "requestAudioFocus")
@@ -10802,11 +10802,11 @@
             //TODO move inside HardeningEnforcer after refactor that moves permission checks
             //     in the blockFocusMethod
             if (permissionOverridesCheck) {
-                mHardeningEnforcer.metricsLogFocusReq(/*blocked*/false, durationHint, uid);
+                mHardeningEnforcer.metricsLogFocusReq(/*blocked*/false, focusReqType, uid);
             }
             if (!permissionOverridesCheck && mHardeningEnforcer.blockFocusMethod(uid,
                     HardeningEnforcer.METHOD_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS,
-                    clientId, durationHint, callingPackageName, attributionTag, sdk)) {
+                    clientId, focusReqType, callingPackageName, attributionTag, sdk)) {
                 final String reason = "Audio focus request blocked by hardening";
                 Log.w(TAG, reason);
                 mmi.set(MediaMetrics.Property.EARLY_RETURN, reason).record();
@@ -10817,14 +10817,14 @@
         }
 
         mmi.record();
-        return mMediaFocusControl.requestAudioFocus(aa, durationHint, cb, fd,
+        return mMediaFocusControl.requestAudioFocus(aa, focusReqType, cb, fd,
                 clientId, callingPackageName, flags, sdk,
-                forceFocusDuckingForAccessibility(aa, durationHint, uid), -1 /*testUid, ignored*/,
+                forceFocusDuckingForAccessibility(aa, focusReqType, uid), -1 /*testUid, ignored*/,
                 permissionOverridesCheck);
     }
 
     /** see {@link AudioManager#requestAudioFocusForTest(AudioFocusRequest, String, int, int)} */
-    public int requestAudioFocusForTest(AudioAttributes aa, int durationHint, IBinder cb,
+    public int requestAudioFocusForTest(AudioAttributes aa, int focusReqType, IBinder cb,
             IAudioFocusDispatcher fd, String clientId, String callingPackageName,
             int flags, int fakeUid, int sdk) {
         if (!enforceQueryAudioStateForTest("focus request")) {
@@ -10835,7 +10835,7 @@
             Log.e(TAG, reason);
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
-        return mMediaFocusControl.requestAudioFocus(aa, durationHint, cb, fd,
+        return mMediaFocusControl.requestAudioFocus(aa, focusReqType, cb, fd,
                 clientId, callingPackageName, flags,
                 sdk, false /*forceDuck*/, fakeUid, true /*permissionOverridesCheck*/);
     }
diff --git a/services/core/java/com/android/server/audio/HardeningEnforcer.java b/services/core/java/com/android/server/audio/HardeningEnforcer.java
index 3c509bc..faeba5d 100644
--- a/services/core/java/com/android/server/audio/HardeningEnforcer.java
+++ b/services/core/java/com/android/server/audio/HardeningEnforcer.java
@@ -155,14 +155,14 @@
      * Checks whether the call in the current thread should be allowed or blocked
      * @param focusMethod name of the method to check, for logging purposes
      * @param clientId id of the requester
-     * @param durationHint focus type being requested
+     * @param focusReqType focus type being requested
      * @param attributionTag attribution of the caller
      * @param targetSdk target SDK of the caller
      * @return false if the method call is allowed, true if it should be a no-op
      */
     @SuppressWarnings("AndroidFrameworkCompatChange")
     protected boolean blockFocusMethod(int callingUid, int focusMethod, @NonNull String clientId,
-            int durationHint, @NonNull String packageName, String attributionTag, int targetSdk) {
+            int focusReqType, @NonNull String packageName, String attributionTag, int targetSdk) {
         if (packageName.isEmpty()) {
             packageName = getPackNameForUid(callingUid);
         }
@@ -181,14 +181,14 @@
             blocked = false;
         }
 
-        metricsLogFocusReq(blocked, durationHint, callingUid);
+        metricsLogFocusReq(blocked, focusReqType, callingUid);
 
         if (!blocked) {
             return false;
         }
 
         String errorMssg = "Focus request DENIED for uid:" + callingUid
-                + " clientId:" + clientId + " req:" + durationHint
+                + " clientId:" + clientId + " req:" + focusReqType
                 + " procState:" + mActivityManager.getUidProcessState(callingUid);
         mEventLogger.enqueueAndSlog(errorMssg, EventLogger.Event.ALOGI, TAG);
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index ffb2bb6..54bc6fb 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1583,6 +1583,8 @@
                     // respond to direct replies with updates. So we need to update System UI
                     // immediately.
                     if (lifetimeExtensionRefactor()) {
+                        // We need to reset this to allow the notif to be updated again.
+                        r.setCanceledAfterLifetimeExtension(false);
                         maybeNotifySystemUiListenerLifetimeExtendedLocked(r,
                                 r.getSbn().getPackageName(), packageImportance);
                     }
@@ -1639,9 +1641,12 @@
                     // respond to direct replies with updates. So we need to update System UI
                     // immediately.
                     if (lifetimeExtensionRefactor()) {
+                        // We need to reset this to allow the notif to be updated again.
+                        r.setCanceledAfterLifetimeExtension(false);
                         maybeNotifySystemUiListenerLifetimeExtendedLocked(r,
                                 r.getSbn().getPackageName(), packageImportance);
                     }
+
                     r.recordSmartReplied();
                     LogMaker logMaker = r.getLogMaker()
                             .setCategory(MetricsEvent.SMART_REPLY_ACTION)
@@ -11741,17 +11746,37 @@
     private void maybeNotifySystemUiListenerLifetimeExtendedLocked(NotificationRecord record,
             String pkg, int packageImportance) {
         if (record != null && (record.getSbn().getNotification().flags
-                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
+                && !record.isCanceledAfterLifetimeExtension()) {
             boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND;
 
-            // Lifetime extended notifications don't need to alert on state change.
+            // Save the original Record's post silently value, so we can restore it after we send
+            // the SystemUI specific silent update.
+            boolean savedPostSilentlyState = record.shouldPostSilently();
+            boolean savedOnlyAlertOnceState = (record.getNotification().flags
+                    & FLAG_ONLY_ALERT_ONCE) > 0;
+            // Lifetime extended notifications don't need to alert on new state change.
             record.setPostSilently(true);
             // We also set FLAG_ONLY_ALERT_ONCE to avoid the notification from HUN-ing again.
             record.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
 
+            PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(null);
+            tracker.addCleanupRunnable(() -> {
+                synchronized (mNotificationLock) {
+                    // Mark that the notification has been updated due to cancelation, so it won't
+                    // be updated again if the app cancels multiple times.
+                    record.setCanceledAfterLifetimeExtension(true);
+                    // Set the post silently status to the record's previous value.
+                    record.setPostSilently(savedPostSilentlyState);
+                    // Remove FLAG_ONLY_ALERT_ONCE if the notification did not previously have it.
+                    if (!savedOnlyAlertOnceState) {
+                        record.getNotification().flags &= ~FLAG_ONLY_ALERT_ONCE;
+                    }
+                }
+            });
+
             mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(),
-                    record, isAppForeground, /* isAppProvided= */ false,
-                    mPostNotificationTrackerFactory.newTracker(null)));
+                    record, isAppForeground, /* isAppProvided= */ false, tracker));
         }
     }
 
@@ -13351,17 +13376,23 @@
         @ElapsedRealtimeLong private final long mStartTime;
         @Nullable private final WakeLock mWakeLock;
         private boolean mOngoing;
+        private final List<Runnable> mCleanupRunnables;
 
         @VisibleForTesting
         PostNotificationTracker(@Nullable WakeLock wakeLock) {
             mStartTime = SystemClock.elapsedRealtime();
             mWakeLock = wakeLock;
             mOngoing = true;
+            mCleanupRunnables = new ArrayList<Runnable>();
             if (DBG) {
                 Slog.d(TAG, "PostNotification: Started");
             }
         }
 
+        void addCleanupRunnable(Runnable runnable) {
+            mCleanupRunnables.add(runnable);
+        }
+
         @ElapsedRealtimeLong
         long getStartTime() {
             return mStartTime;
@@ -13373,8 +13404,9 @@
         }
 
         /**
-         * Cancels the tracker (releasing the acquired WakeLock). Either {@link #finish} or
-         * {@link #cancel} (exclusively) should be called on this object before it's discarded.
+         * Cancels the tracker (releasing the acquired WakeLock) and runs any set cleanup runnables.
+         * Either {@link #finish} or {@link #cancel} (exclusively) should be called on this object
+         * before it's discarded.
          */
         void cancel() {
             if (!isOngoing()) {
@@ -13385,6 +13417,9 @@
             if (mWakeLock != null) {
                 Binder.withCleanCallingIdentity(() -> mWakeLock.release());
             }
+            for (Runnable r : mCleanupRunnables) {
+                r.run();
+            }
             if (DBG) {
                 long elapsedTime = SystemClock.elapsedRealtime() - mStartTime;
                 Slog.d(TAG, TextUtils.formatSimple("PostNotification: Abandoned after %d ms",
@@ -13393,9 +13428,10 @@
         }
 
         /**
-         * Finishes the tracker (releasing the acquired WakeLock) and returns the time elapsed since
-         * the operation started, in milliseconds. Either {@link #finish} or {@link #cancel}
-         * (exclusively) should be called on this object before it's discarded.
+         * Finishes the tracker (releasing the acquired WakeLock), runs any set cleanup runnables,
+         * and returns the time elapsed since the operation started, in milliseconds.
+         * Either {@link #finish} or {@link #cancel} (exclusively) should be called on this object
+         * before it's discarded.
          */
         @DurationMillisLong
         long finish() {
@@ -13408,6 +13444,9 @@
             if (mWakeLock != null) {
                 Binder.withCleanCallingIdentity(() -> mWakeLock.release());
             }
+            for (Runnable r : mCleanupRunnables) {
+                r.run();
+            }
             if (DBG) {
                 Slog.d(TAG,
                         TextUtils.formatSimple("PostNotification: Finished in %d ms", elapsedTime));
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 1392003..e541246 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -222,6 +222,9 @@
     private boolean mPendingLogUpdate = false;
     private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
     private boolean mSensitiveContent = false;
+    // Whether an app has attempted to cancel this notification after it has been marked as
+    // lifetime extended.
+    private boolean mCanceledAfterLifetimeExtension = false;
 
     public NotificationRecord(Context context, StatusBarNotification sbn,
             NotificationChannel channel) {
@@ -535,6 +538,7 @@
                 + NotificationListenerService.Ranking.importanceToString(mProposedImportance));
         pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked);
         pw.println(prefix + "mSensitiveContent=" + mSensitiveContent);
+        pw.println(prefix + "mCanceledAfterLifetimeExtension=" + mCanceledAfterLifetimeExtension);
         pw.println(prefix + "mIntercept=" + mIntercept);
         pw.println(prefix + "mHidden==" + mHidden);
         pw.println(prefix + "mGlobalSortKey=" + mGlobalSortKey);
@@ -1620,6 +1624,14 @@
         mPkgAllowedAsConvo = allowedAsConvo;
     }
 
+    public boolean isCanceledAfterLifetimeExtension() {
+        return mCanceledAfterLifetimeExtension;
+    }
+
+    public void setCanceledAfterLifetimeExtension(boolean canceledAfterLifetimeExtension) {
+        mCanceledAfterLifetimeExtension = canceledAfterLifetimeExtension;
+    }
+
     /**
      * Whether this notification is a conversation notification.
      */
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 7f24769..822ec2e 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -1644,8 +1644,7 @@
                         if (Flags.allowThermalHeadroomThresholds()) {
                             for (int severity = ThrottlingSeverity.LIGHT;
                                     severity <= ThrottlingSeverity.SHUTDOWN; severity++) {
-                                if (severity != ThrottlingSeverity.SEVERE
-                                        && threshold.hotThrottlingThresholds.length > severity) {
+                                if (threshold.hotThrottlingThresholds.length > severity) {
                                     updateHeadroomThreshold(severity,
                                             threshold.hotThrottlingThresholds[severity],
                                             severeThreshold);
diff --git a/services/core/java/com/android/server/wm/TrustedOverlayHost.java b/services/core/java/com/android/server/wm/TrustedOverlayHost.java
index 9b868be..5f3c558 100644
--- a/services/core/java/com/android/server/wm/TrustedOverlayHost.java
+++ b/services/core/java/com/android/server/wm/TrustedOverlayHost.java
@@ -112,11 +112,16 @@
         final SurfaceControl.Transaction t = mWmService.mTransactionFactory.get();
 
         for (int i = mOverlays.size() - 1; i >= 0; i--) {
-           SurfaceControlViewHost.SurfacePackage l = mOverlays.get(i);
-           if (l.getSurfaceControl().isSameSurface(p.getSurfaceControl())) {
-               mOverlays.remove(i);
-               t.reparent(l.getSurfaceControl(), null);
-               l.release();
+            SurfaceControlViewHost.SurfacePackage l = mOverlays.get(i);
+            SurfaceControl overlaySurfaceControl = l.getSurfaceControl();
+            if (overlaySurfaceControl == null) {
+                // Remove the overlay if the surfacepackage was released. Ownership
+                // is shared, so this may happen.
+                mOverlays.remove(i);
+            } else if (overlaySurfaceControl.isSameSurface(p.getSurfaceControl())) {
+                mOverlays.remove(i);
+                t.reparent(l.getSurfaceControl(), null);
+                l.release();
            }
         }
         t.apply();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 5a8de58..0a52238 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -3047,6 +3047,41 @@
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public void testMultipleCancelOfLifetimeExtendedSendsOneUpdate() throws Exception {
+        final NotificationRecord notif = generateNotificationRecord(null);
+        notif.getSbn().getNotification().flags =
+                Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(notif);
+        final StatusBarNotification sbn = notif.getSbn();
+
+        assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
+        assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
+
+        // Send two cancelations.
+        mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(),
+                sbn.getUserId());
+        waitForIdle();
+        mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(),
+                sbn.getUserId());
+        waitForIdle();
+
+        assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
+        assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
+
+        // Checks that only one post update is sent.
+        verify(mWorkerHandler, times(1))
+                .post(any(NotificationManagerService.PostNotificationRunnable.class));
+        ArgumentCaptor<NotificationRecord> captor =
+                ArgumentCaptor.forClass(NotificationRecord.class);
+        verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+                anyBoolean());
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
     public void testCancelAllClearsLifetimeExtended() throws Exception {
         final NotificationRecord notif = generateNotificationRecord(
                 mTestNotificationChannel, 1, "group", true);
@@ -6419,12 +6454,31 @@
     @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
     public void testStats_DirectReplyLifetimeExtendedPostsUpdate() throws Exception {
         final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+        // Marks the notification as having already been lifetime extended and canceled.
         r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        r.setCanceledAfterLifetimeExtension(true);
+        r.setPostSilently(true);
         mService.addNotification(r);
 
         mService.mNotificationDelegate.onNotificationDirectReplied(r.getKey());
         waitForIdle();
 
+        // At the moment prepareNotifyPostedLocked is called on the listeners,
+        // verify that FLAG_ONLY_ALERT_ONCE and shouldPostSilently are set, regardless of initial
+        // values.
+        doAnswer(
+                invocation -> {
+                    int flags = ((NotificationRecord) invocation.getArgument(0))
+                            .getSbn().getNotification().flags;
+                    assertThat(flags & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
+                    boolean shouldPostSilently = ((NotificationRecord) invocation.getArgument(0))
+                            .shouldPostSilently();
+                    assertThat(shouldPostSilently).isTrue();
+                    return null;
+                }
+        ).when(mListeners).prepareNotifyPostedLocked(any(), any(), anyBoolean());
+
+        // Checks that the record gets marked as a direct reply having occurred.
         assertThat(mService.getNotificationRecord(r.getKey()).getStats().hasDirectReplied())
                 .isTrue();
         // Checks that a post update is sent.
@@ -6437,9 +6491,65 @@
         assertThat(captor.getValue().getNotification().flags
                 & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                 FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+        // FLAG_ONLY_ALERT_ONCE was not present on the original notification, so it's not here.
         assertThat(captor.getValue().getNotification().flags
-                & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
+                & FLAG_ONLY_ALERT_ONCE).isEqualTo(0);
         assertThat(captor.getValue().shouldPostSilently()).isTrue();
+        assertThat(captor.getValue().isCanceledAfterLifetimeExtension()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public void testStats_DirectReplyLifetimeExtendedPostsUpdate_RestorePostSilently()
+            throws Exception {
+        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+        // Marks the notification as having already been lifetime extended and canceled.
+        r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        r.setPostSilently(false);
+        mService.addNotification(r);
+
+        mService.mNotificationDelegate.onNotificationDirectReplied(r.getKey());
+        waitForIdle();
+
+        // Checks that a post update is sent with shouldPostSilently set to true.
+        doAnswer(
+                invocation -> {
+                    boolean shouldPostSilently = ((NotificationRecord) invocation.getArgument(0))
+                            .shouldPostSilently();
+                    assertThat(shouldPostSilently).isTrue();
+                    return null;
+                }
+        ).when(mListeners).prepareNotifyPostedLocked(any(), any(), anyBoolean());
+
+        // Checks that shouldPostSilently is restored to its false state afterward.
+        assertThat(mService.getNotificationRecord(r.getKey()).shouldPostSilently()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public void testStats_DirectReplyLifetimeExtendedPostsUpdate_RestoreOnlyAlertOnceFlag()
+            throws Exception {
+        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+        // Marks the notification as having already been lifetime extended and canceled.
+        r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(r);
+
+        mService.mNotificationDelegate.onNotificationDirectReplied(r.getKey());
+        waitForIdle();
+
+        // Checks that a post update is sent with FLAG_ONLY_ALERT_ONCE set to true.
+        doAnswer(
+                invocation -> {
+                    int flags = ((NotificationRecord) invocation.getArgument(0))
+                            .getSbn().getNotification().flags;
+                    assertThat(flags & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
+                    return null;
+                }
+        ).when(mListeners).prepareNotifyPostedLocked(any(), any(), anyBoolean());
+
+        // Checks that the flag is removed afterward.
+        assertThat(mService.getNotificationRecord(r.getKey()).getSbn().getNotification().flags
+                & FLAG_ONLY_ALERT_ONCE).isEqualTo(0);
     }
 
     @Test
@@ -6476,6 +6586,7 @@
                 anyBoolean());
         assertThat(captor.getValue().getNotification().flags
                 & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(0);
+        assertThat(captor.getValue().isCanceledAfterLifetimeExtension()).isFalse();
         assertThat(captor.getValue()
                 .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE).toString())
                 .isEqualTo("new title");
@@ -9143,11 +9254,13 @@
         final int replyIndex = 2;
         final String reply = "Hello";
         final boolean modifiedBeforeSending = true;
-        final boolean generatedByAssistant = true;
 
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
         r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
-        r.setSuggestionsGeneratedByAssistant(generatedByAssistant);
+        r.getSbn().getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
+        r.setSuggestionsGeneratedByAssistant(true);
+        r.setCanceledAfterLifetimeExtension(true);
+        r.setPostSilently(true);
         mService.addNotification(r);
 
         mService.mNotificationDelegate.onNotificationSmartReplySent(
@@ -9155,6 +9268,21 @@
                 modifiedBeforeSending);
         waitForIdle();
 
+        // At the moment prepareNotifyPostedLocked is called on the listeners,
+        // verify that FLAG_ONLY_ALERT_ONCE and shouldPostSilently are set, regardless of initial
+        // values.
+        doAnswer(
+                invocation -> {
+                    int flags = ((NotificationRecord) invocation.getArgument(0))
+                            .getSbn().getNotification().flags;
+                    assertThat(flags & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
+                    boolean shouldPostSilently = ((NotificationRecord) invocation.getArgument(0))
+                            .shouldPostSilently();
+                    assertThat(shouldPostSilently).isTrue();
+                    return null;
+                }
+        ).when(mListeners).prepareNotifyPostedLocked(any(), any(), anyBoolean());
+
         // Checks that a post update is sent.
         verify(mWorkerHandler, times(1))
                 .post(any(NotificationManagerService.PostNotificationRunnable.class));
@@ -9165,8 +9293,10 @@
         assertThat(captor.getValue().getNotification().flags
                 & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                 FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+        // Flag was present before, so it's set afterward
         assertThat(captor.getValue().getNotification().flags
                 & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
+        // Should post silently was set before, so it's set afterward.
         assertThat(captor.getValue().shouldPostSilently()).isTrue();
     }