Merge "Fix NPE caused by null replacedPkgSetting" into main
diff --git a/Android.bp b/Android.bp
index 127556f..e7c2041 100644
--- a/Android.bp
+++ b/Android.bp
@@ -427,6 +427,7 @@
         "framework-permission-aidl-java",
         "spatializer-aidl-java",
         "audiopolicy-aidl-java",
+        "volumegroupcallback-aidl-java",
         "sounddose-aidl-java",
         "modules-utils-expresslog",
         "perfetto_trace_javastream_protos_jarjar",
diff --git a/boot/boot-image-profile.txt b/boot/boot-image-profile.txt
index d7c409f..4c3e5dc 100644
--- a/boot/boot-image-profile.txt
+++ b/boot/boot-image-profile.txt
@@ -28829,7 +28829,6 @@
 Landroid/media/audiopolicy/AudioProductStrategy;
 Landroid/media/audiopolicy/AudioVolumeGroup$1;
 Landroid/media/audiopolicy/AudioVolumeGroup;
-Landroid/media/audiopolicy/AudioVolumeGroupChangeHandler;
 Landroid/media/audiopolicy/IAudioPolicyCallback$Stub$Proxy;
 Landroid/media/audiopolicy/IAudioPolicyCallback$Stub;
 Landroid/media/audiopolicy/IAudioPolicyCallback;
diff --git a/boot/preloaded-classes b/boot/preloaded-classes
index 7f4b324..0486877 100644
--- a/boot/preloaded-classes
+++ b/boot/preloaded-classes
@@ -5509,7 +5509,6 @@
 android.media.audiopolicy.AudioProductStrategy
 android.media.audiopolicy.AudioVolumeGroup$1
 android.media.audiopolicy.AudioVolumeGroup
-android.media.audiopolicy.AudioVolumeGroupChangeHandler
 android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy
 android.media.audiopolicy.IAudioPolicyCallback$Stub
 android.media.audiopolicy.IAudioPolicyCallback
diff --git a/config/preloaded-classes b/config/preloaded-classes
index 707acb0..3f0e00b 100644
--- a/config/preloaded-classes
+++ b/config/preloaded-classes
@@ -5514,7 +5514,6 @@
 android.media.audiopolicy.AudioProductStrategy
 android.media.audiopolicy.AudioVolumeGroup$1
 android.media.audiopolicy.AudioVolumeGroup
-android.media.audiopolicy.AudioVolumeGroupChangeHandler
 android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy
 android.media.audiopolicy.IAudioPolicyCallback$Stub
 android.media.audiopolicy.IAudioPolicyCallback
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 2ce3609..95b9b49 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2940,6 +2940,13 @@
 
 package android.app.supervision {
 
+  @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public class SupervisionAppService extends android.app.Service {
+    ctor public SupervisionAppService();
+    method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onDisabled();
+    method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onEnabled();
+  }
+
   @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager {
     method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public android.content.Intent createConfirmSupervisionCredentialsIntent();
     method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled();
@@ -7435,7 +7442,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
-    method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
+    method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
     method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void removeOnDevicesForAttributesChangedListener(@NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener);
@@ -7466,7 +7473,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
-    method public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback);
+    method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback);
     field public static final String ACTION_VOLUME_CHANGED = "android.media.VOLUME_CHANGED_ACTION";
     field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1
     field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4
diff --git a/core/java/android/app/supervision/SupervisionAppService.java b/core/java/android/app/supervision/SupervisionAppService.java
index 4530be5..93eb962 100644
--- a/core/java/android/app/supervision/SupervisionAppService.java
+++ b/core/java/android/app/supervision/SupervisionAppService.java
@@ -16,7 +16,11 @@
 
 package android.app.supervision;
 
+import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.app.Service;
+import android.app.supervision.flags.Flags;
 import android.content.Intent;
 import android.os.IBinder;
 
@@ -26,31 +30,43 @@
  *
  * @hide
  */
+@SystemApi
+@FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
 public class SupervisionAppService extends Service {
-    private final ISupervisionAppService mBinder = new ISupervisionAppService.Stub() {
-        @Override
-        public void onEnabled() {
-            SupervisionAppService.this.onEnabled();
-        }
+    private final ISupervisionAppService mBinder =
+            new ISupervisionAppService.Stub() {
+                @Override
+                public void onEnabled() {
+                    SupervisionAppService.this.onEnabled();
+                }
 
-        @Override
-        public void onDisabled() {
-            SupervisionAppService.this.onDisabled();
-        }
-    };
+                @Override
+                public void onDisabled() {
+                    SupervisionAppService.this.onDisabled();
+                }
+            };
 
+    @Nullable
     @Override
-    public final IBinder onBind(Intent intent) {
+    public final IBinder onBind(@Nullable Intent intent) {
         return mBinder.asBinder();
     }
 
     /**
      * Called when supervision is enabled.
+     *
+     * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
     public void onEnabled() {}
 
     /**
      * Called when supervision is disabled.
+     *
+     * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
     public void onDisabled() {}
 }
diff --git a/core/java/android/util/ArrayMap.java b/core/java/android/util/ArrayMap.java
index 7ee0ff1..c599079 100644
--- a/core/java/android/util/ArrayMap.java
+++ b/core/java/android/util/ArrayMap.java
@@ -129,7 +129,7 @@
             return ContainerHelpers.binarySearch(hashes, N, hash);
         } catch (ArrayIndexOutOfBoundsException e) {
             if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
-                throw new ConcurrentModificationException();
+                throw new ConcurrentModificationException(e);
             } else {
                 throw e; // the cache is poisoned at this point, there's not much we can do
             }
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 4bd7d16..4bd0d97 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -103,7 +103,7 @@
     ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
     ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
             true),
-    ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false),
+    ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, true),
     ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX(
             Flags::enableDragToDesktopIncomingTransitionsBugfix, false),
     ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index bfa0aa9..7ed73d7 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -210,7 +210,6 @@
                 "android_media_AudioAttributes.cpp",
                 "android_media_AudioProductStrategies.cpp",
                 "android_media_AudioVolumeGroups.cpp",
-                "android_media_AudioVolumeGroupCallback.cpp",
                 "android_media_DeviceCallback.cpp",
                 "android_media_MediaMetricsJNI.cpp",
                 "android_media_MicrophoneInfo.cpp",
@@ -311,6 +310,7 @@
                 "audioflinger-aidl-cpp",
                 "audiopolicy-types-aidl-cpp",
                 "spatializer-aidl-cpp",
+                "volumegroupcallback-aidl-cpp",
                 "av-types-aidl-cpp",
                 "android.hardware.camera.device@3.2",
                 "camera_platform_flags_c_lib",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index b2b8263..1ff0774 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -101,7 +101,6 @@
 extern int register_android_media_AudioAttributes(JNIEnv *env);
 extern int register_android_media_AudioProductStrategies(JNIEnv *env);
 extern int register_android_media_AudioVolumeGroups(JNIEnv *env);
-extern int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env);
 extern int register_android_media_ImageReader(JNIEnv *env);
 extern int register_android_media_ImageWriter(JNIEnv *env);
 extern int register_android_media_MicrophoneInfo(JNIEnv *env);
@@ -1660,7 +1659,6 @@
         REG_JNI(register_android_media_AudioAttributes),
         REG_JNI(register_android_media_AudioProductStrategies),
         REG_JNI(register_android_media_AudioVolumeGroups),
-        REG_JNI(register_android_media_AudioVolumeGroupChangeHandler),
         REG_JNI(register_android_media_ImageReader),
         REG_JNI(register_android_media_ImageWriter),
         REG_JNI(register_android_media_MediaMetrics),
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index b679688..1bbf811 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -20,16 +20,17 @@
 
 #include <atomic>
 #define LOG_TAG "AudioSystem-JNI"
+#include <android-base/properties.h>
 #include <android/binder_ibinder_jni.h>
 #include <android/binder_libbinder.h>
 #include <android/media/AudioVibratorInfo.h>
+#include <android/media/INativeAudioVolumeGroupCallback.h>
 #include <android/media/INativeSpatializerCallback.h>
 #include <android/media/ISpatializer.h>
 #include <android/media/audio/common/AudioConfigBase.h>
 #include <android_media_audiopolicy.h>
 #include <android_os_Parcel.h>
 #include <audiomanager/AudioManager.h>
-#include <android-base/properties.h>
 #include <binder/IBinder.h>
 #include <jni.h>
 #include <media/AidlConversion.h>
@@ -41,14 +42,14 @@
 #include <nativehelper/ScopedLocalRef.h>
 #include <nativehelper/ScopedPrimitiveArray.h>
 #include <nativehelper/jni_macros.h>
+#include <sys/system_properties.h>
 #include <system/audio.h>
 #include <system/audio_policy.h>
-#include <sys/system_properties.h>
 #include <utils/Log.h>
 
+#include <memory>
 #include <optional>
 #include <sstream>
-#include <memory>
 #include <vector>
 
 #include "android_media_AudioAttributes.h"
@@ -59,8 +60,8 @@
 #include "android_media_AudioFormat.h"
 #include "android_media_AudioMixerAttributes.h"
 #include "android_media_AudioProfile.h"
-#include "android_media_MicrophoneInfo.h"
 #include "android_media_JNIUtils.h"
+#include "android_media_MicrophoneInfo.h"
 #include "android_util_Binder.h"
 #include "core_jni_helpers.h"
 
@@ -3442,6 +3443,21 @@
     }
 }
 
+static int android_media_AudioSystem_registerAudioVolumeGroupCallback(
+        JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) {
+    sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback =
+            interface_cast<media::INativeAudioVolumeGroupCallback>(
+                    ibinderForJavaObject(env, jIAudioVolumeGroupCallback));
+    return AudioSystem::addAudioVolumeGroupCallback(nIAudioVolumeGroupCallback);
+}
+
+static int android_media_AudioSystem_unregisterAudioVolumeGroupCallback(
+        JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) {
+    sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback =
+            interface_cast<media::INativeAudioVolumeGroupCallback>(
+                    ibinderForJavaObject(env, jIAudioVolumeGroupCallback));
+    return AudioSystem::removeAudioVolumeGroupCallback(nIAudioVolumeGroupCallback);
+}
 
 // ----------------------------------------------------------------------------
 
@@ -3612,6 +3628,12 @@
         MAKE_JNI_NATIVE_METHOD("clearPreferredMixerAttributes",
                                "(Landroid/media/AudioAttributes;II)I",
                                android_media_AudioSystem_clearPreferredMixerAttributes),
+        MAKE_JNI_NATIVE_METHOD("registerAudioVolumeGroupCallback",
+                               "(Landroid/media/INativeAudioVolumeGroupCallback;)I",
+                               android_media_AudioSystem_registerAudioVolumeGroupCallback),
+        MAKE_JNI_NATIVE_METHOD("unregisterAudioVolumeGroupCallback",
+                               "(Landroid/media/INativeAudioVolumeGroupCallback;)I",
+                               android_media_AudioSystem_unregisterAudioVolumeGroupCallback),
         MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency),
         MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled),
         MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled),
diff --git a/core/jni/android_media_AudioVolumeGroupCallback.cpp b/core/jni/android_media_AudioVolumeGroupCallback.cpp
deleted file mode 100644
index d130a4b..0000000
--- a/core/jni/android_media_AudioVolumeGroupCallback.cpp
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-#undef ANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION // TODO:remove this and fix code
-
-//#define LOG_NDEBUG 0
-
-#define LOG_TAG "AudioVolumeGroupCallback-JNI"
-
-#include <utils/Log.h>
-#include <nativehelper/JNIHelp.h>
-#include "core_jni_helpers.h"
-
-#include "android_media_AudioVolumeGroupCallback.h"
-
-
-// ----------------------------------------------------------------------------
-using namespace android;
-
-static const char* const kAudioVolumeGroupChangeHandlerClassPathName =
-        "android/media/audiopolicy/AudioVolumeGroupChangeHandler";
-
-static struct {
-    jfieldID    mJniCallback;
-} gAudioVolumeGroupChangeHandlerFields;
-
-static struct {
-    jmethodID    postEventFromNative;
-} gAudioVolumeGroupChangeHandlerMethods;
-
-static Mutex gLock;
-
-JNIAudioVolumeGroupCallback::JNIAudioVolumeGroupCallback(JNIEnv* env,
-                                                         jobject thiz,
-                                                         jobject weak_thiz)
-{
-    jclass clazz = env->GetObjectClass(thiz);
-    if (clazz == NULL) {
-        ALOGE("Can't find class %s", kAudioVolumeGroupChangeHandlerClassPathName);
-        return;
-    }
-    mClass = (jclass)env->NewGlobalRef(clazz);
-
-    // We use a weak reference so the AudioVolumeGroupChangeHandler object can be garbage collected.
-    // The reference is only used as a proxy for callbacks.
-    mObject  = env->NewGlobalRef(weak_thiz);
-}
-
-JNIAudioVolumeGroupCallback::~JNIAudioVolumeGroupCallback()
-{
-    // remove global references
-    JNIEnv *env = AndroidRuntime::getJNIEnv();
-    if (env == NULL) {
-        return;
-    }
-    env->DeleteGlobalRef(mObject);
-    env->DeleteGlobalRef(mClass);
-}
-
-void JNIAudioVolumeGroupCallback::onAudioVolumeGroupChanged(volume_group_t group, int flags)
-{
-    JNIEnv *env = AndroidRuntime::getJNIEnv();
-    if (env == NULL) {
-        return;
-    }
-    ALOGV("%s volume group id %d", __FUNCTION__, group);
-    env->CallStaticVoidMethod(mClass,
-                              gAudioVolumeGroupChangeHandlerMethods.postEventFromNative,
-                              mObject,
-                              AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED, group, flags, NULL);
-    if (env->ExceptionCheck()) {
-        ALOGW("An exception occurred while notifying an event.");
-        env->ExceptionClear();
-    }
-}
-
-void JNIAudioVolumeGroupCallback::onServiceDied()
-{
-    JNIEnv *env = AndroidRuntime::getJNIEnv();
-    if (env == NULL) {
-        return;
-    }
-    env->CallStaticVoidMethod(mClass,
-                              gAudioVolumeGroupChangeHandlerMethods.postEventFromNative,
-                              mObject,
-                              AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED, 0, 0, NULL);
-    if (env->ExceptionCheck()) {
-        ALOGW("An exception occurred while notifying an event.");
-        env->ExceptionClear();
-    }
-}
-
-static
-sp<JNIAudioVolumeGroupCallback> setJniCallback(JNIEnv* env,
-                                               jobject thiz,
-                                               const sp<JNIAudioVolumeGroupCallback>& callback)
-{
-    Mutex::Autolock l(gLock);
-    sp<JNIAudioVolumeGroupCallback> old = (JNIAudioVolumeGroupCallback*)env->GetLongField(
-                thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback);
-    if (callback.get()) {
-        callback->incStrong((void*)setJniCallback);
-    }
-    if (old != 0) {
-        old->decStrong((void*)setJniCallback);
-    }
-    env->SetLongField(thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback,
-                      (jlong)callback.get());
-    return old;
-}
-
-static void
-android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup(JNIEnv *env,
-                                                              jobject thiz,
-                                                              jobject weak_this)
-{
-    ALOGV("%s", __FUNCTION__);
-    sp<JNIAudioVolumeGroupCallback> callback =
-            new JNIAudioVolumeGroupCallback(env, thiz, weak_this);
-
-    if (AudioSystem::addAudioVolumeGroupCallback(callback) == NO_ERROR) {
-        setJniCallback(env, thiz, callback);
-    }
-}
-
-static void
-android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize(JNIEnv *env, jobject thiz)
-{
-    ALOGV("%s", __FUNCTION__);
-    sp<JNIAudioVolumeGroupCallback> callback = setJniCallback(env, thiz, 0);
-    if (callback != 0) {
-        AudioSystem::removeAudioVolumeGroupCallback(callback);
-    }
-}
-
-/*
- * JNI registration.
- */
-static const JNINativeMethod gMethods[] = {
-    {"native_setup", "(Ljava/lang/Object;)V",
-        (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup},
-    {"native_finalize",  "()V",
-        (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize},
-};
-
-int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env)
-{
-    jclass audioVolumeGroupChangeHandlerClass =
-            FindClassOrDie(env, kAudioVolumeGroupChangeHandlerClassPathName);
-    gAudioVolumeGroupChangeHandlerMethods.postEventFromNative =
-            GetStaticMethodIDOrDie(env, audioVolumeGroupChangeHandlerClass, "postEventFromNative",
-                                   "(Ljava/lang/Object;IIILjava/lang/Object;)V");
-
-    gAudioVolumeGroupChangeHandlerFields.mJniCallback =
-            GetFieldIDOrDie(env, audioVolumeGroupChangeHandlerClass, "mJniCallback", "J");
-
-    env->DeleteLocalRef(audioVolumeGroupChangeHandlerClass);
-
-    return RegisterMethodsOrDie(env,
-                                kAudioVolumeGroupChangeHandlerClassPathName,
-                                gMethods,
-                                NELEM(gMethods));
-}
-
diff --git a/core/jni/android_media_AudioVolumeGroupCallback.h b/core/jni/android_media_AudioVolumeGroupCallback.h
deleted file mode 100644
index de06549..0000000
--- a/core/jni/android_media_AudioVolumeGroupCallback.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#pragma once
-
-#include <system/audio.h>
-#include <media/AudioSystem.h>
-
-namespace android {
-
-// keep in sync with AudioManager.AudioVolumeGroupChangeHandler.java
-#define AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED      1000
-#define AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED        1001
-
-class JNIAudioVolumeGroupCallback: public AudioSystem::AudioVolumeGroupCallback
-{
-public:
-    JNIAudioVolumeGroupCallback(JNIEnv* env, jobject thiz, jobject weak_thiz);
-    ~JNIAudioVolumeGroupCallback();
-
-    void onAudioVolumeGroupChanged(volume_group_t group, int flags) override;
-    void onServiceDied() override;
-
-private:
-    void sendEvent(int event);
-
-    jclass      mClass; /**< Reference to AudioVolumeGroupChangeHandler class. */
-    jobject     mObject; /**< Weak ref to AudioVolumeGroupChangeHandler object to call on. */
-};
-
-} // namespace android
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index e47adc9..1a74fe6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1219,6 +1219,8 @@
             6 - Lock if keyguard enabled or go to sleep (doze)
             7 - Dream if possible or go to sleep (doze)
             8 - Go to glanceable hub or dream if possible, or sleep if neither is available (doze)
+            9 - Go to dream if device is not dreaming, stop dream if device is dreaming, or sleep if
+                neither is available (doze)
     -->
     <integer name="config_shortPressOnPowerBehavior">1</integer>
 
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index ef6b918..849ca28 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -86,7 +86,7 @@
          CarrierConfigManager#KEY_AUTO_DATA_SWITCH_RAT_SIGNAL_SCORE_STRING_ARRAY.
          If 0, the device always switch to the higher score SIM.
          If < 0, the network type and signal strength based auto switch is disabled. -->
-    <integer name="auto_data_switch_score_tolerance">4000</integer>
+    <integer name="auto_data_switch_score_tolerance">7000</integer>
     <java-symbol type="integer" name="auto_data_switch_score_tolerance" />
 
     <!-- Boolean indicating whether the Iwlan data service supports persistence of iwlan ipsec
@@ -261,7 +261,9 @@
          to identify providers that should be ignored if the carrier config
          carrier_supported_satellite_services_per_provider_bundle does not support them.
          -->
-    <string-array name="config_satellite_providers" translatable="false"></string-array>
+    <string-array name="config_satellite_providers" translatable="false">
+        <item>"310830"</item>
+    </string-array>
     <java-symbol type="array" name="config_satellite_providers" />
 
     <!-- The identifier of the satellite's SIM profile. The identifier is composed of MCC and MNC
diff --git a/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt b/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt
new file mode 100644
index 0000000..e80d3a6
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2025 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.internal.app
+
+import android.content.Context
+import android.media.MediaRouter
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class MediaRouteDialogPresenterTest {
+    private var selectedRoute: MediaRouter.RouteInfo = mock()
+    private var mediaRouter: MediaRouter = mock<MediaRouter> {
+        on { selectedRoute } doReturn selectedRoute
+    }
+    private var context: Context = mock<Context> {
+        on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE
+        on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter
+    }
+
+    @Test
+    fun shouldShowChooserDialog_routeNotDefault_returnsFalse() {
+        selectedRoute.stub {
+            on { isDefault } doReturn false
+            on { matchesTypes(anyInt()) } doReturn true
+        }
+
+        assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+            context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+            .isEqualTo(false)
+    }
+
+    @Test
+    fun shouldShowChooserDialog_routeDefault_returnsTrue() {
+        selectedRoute.stub {
+            on { isDefault } doReturn true
+            on { matchesTypes(anyInt()) } doReturn true
+        }
+
+        assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+            context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+            .isEqualTo(true)
+    }
+
+    @Test
+    fun shouldShowChooserDialog_routeNotMatch_returnsTrue() {
+        selectedRoute.stub {
+            on { isDefault } doReturn false
+            on { matchesTypes(anyInt()) } doReturn false
+        }
+
+        assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+            context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+            .isEqualTo(true)
+    }
+
+    @Test
+    fun shouldShowChooserDialog_routeDefaultAndNotMatch_returnsTrue() {
+        selectedRoute.stub {
+            on { isDefault } doReturn true
+            on { matchesTypes(anyInt()) } doReturn false
+        }
+
+        assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+            context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+            .isEqualTo(true)
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index ed5e0c6..e5a4cd0 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -278,6 +278,9 @@
         if (!canEnterDesktopMode(context)) {
             return false;
         }
+        if (!enforceDeviceRestrictions()) {
+            return true;
+        }
         if (display.getType() == Display.TYPE_INTERNAL) {
             return canInternalDisplayHostDesktops(context);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
index 9fa1621..d9a66e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
@@ -57,6 +57,11 @@
      * @return 0f = no dim applied. 1f = full black.
      */
     public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) {
+        // On tablets, apps don't go offscreen, so only dim for dismissal.
+        if (!snapAlgorithm.areOffscreenRatiosSupported()) {
+            return ParallaxSpec.super.getDimValue(position, snapAlgorithm);
+        }
+
         int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition();
         int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition();
         int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 2d44395..5f13ba9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -762,6 +762,7 @@
             floatingBtn.isEnabled = !taskInfo.isPinned
             floatingBtn.imageTintList = style.windowingButtonColor
             desktopBtn.isGone = !shouldShowDesktopModeButton
+            desktopBtnSpace.isGone = !shouldShowDesktopModeButton
             desktopBtn.isSelected = taskInfo.isFreeform
             desktopBtn.isEnabled = !taskInfo.isFreeform
             desktopBtn.imageTintList = style.windowingButtonColor
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
index 22a85fc..9f2534e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
@@ -71,6 +71,7 @@
         when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget);
         when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget);
         when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge);
+        when(mockSnapAlgorithm.areOffscreenRatiosSupported()).thenReturn(true);
 
         when(mockStartEdge.getPosition()).thenReturn(0);
         when(mockFirstTarget.getPosition()).thenReturn(250);
diff --git a/libs/hwui/renderthread/ReliableSurface.cpp b/libs/hwui/renderthread/ReliableSurface.cpp
index 01e8010..64d38b9 100644
--- a/libs/hwui/renderthread/ReliableSurface.cpp
+++ b/libs/hwui/renderthread/ReliableSurface.cpp
@@ -149,25 +149,9 @@
         return AHardwareBuffer_to_ANativeWindowBuffer(mScratchBuffer.get());
     }
 
-    int width = -1;
-    int result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_WIDTH, &width);
-    if (result != OK || width < 0) {
-        ALOGW("Failed to query window default width: %s (%d) value=%d", strerror(-result), result,
-              width);
-        width = 1;
-    }
-
-    int height = -1;
-    result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_HEIGHT, &height);
-    if (result != OK || height < 0) {
-        ALOGW("Failed to query window default height: %s (%d) value=%d", strerror(-result), result,
-              height);
-        height = 1;
-    }
-
     AHardwareBuffer_Desc desc = AHardwareBuffer_Desc{
-            .width = static_cast<uint32_t>(width),
-            .height = static_cast<uint32_t>(height),
+            .width = 1,
+            .height = 1,
             .layers = 1,
             .format = mFormat,
             .usage = mUsage,
@@ -176,9 +160,9 @@
     };
 
     AHardwareBuffer* newBuffer;
-    result = AHardwareBuffer_allocate(&desc, &newBuffer);
+    int result = AHardwareBuffer_allocate(&desc, &newBuffer);
 
-    if (result != OK) {
+    if (result != NO_ERROR) {
         // Allocate failed, that sucks
         ALOGW("Failed to allocate scratch buffer, error=%d", result);
         return nullptr;
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index a67aea4..0cd9c53 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -238,6 +238,7 @@
     for (uint32_t i = 0; i < queueCount; i++) {
         queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT;
         queuePriorityProps[i].pNext = nullptr;
+        queueProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2;
         queueProps[i].pNext = &queuePriorityProps[i];
     }
     mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get());
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4aba491..0a1bfd55 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -19,14 +19,15 @@
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
 import static android.content.Context.DEVICE_ID_DEFAULT;
+import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO;
+import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
+import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
+import static android.media.audio.Flags.FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING;
+import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API;
 import static android.media.audio.Flags.FLAG_UNIFY_ABSOLUTE_VOLUME_MANAGEMENT;
 import static android.media.audio.Flags.autoPublicVolumeApiHardening;
 import static android.media.audio.Flags.cacheGetStreamMinMaxVolume;
 import static android.media.audio.Flags.cacheGetStreamVolume;
-import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO;
-import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
-import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
-import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API;
 import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.Manifest;
@@ -65,7 +66,7 @@
 import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
-import android.media.audiopolicy.AudioVolumeGroupChangeHandler;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
 import android.media.projection.MediaProjection;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
@@ -128,8 +129,6 @@
     private static final String TAG = "AudioManager";
     private static final boolean DEBUG = false;
     private static final AudioPortEventHandler sAudioPortEventHandler = new AudioPortEventHandler();
-    private static final AudioVolumeGroupChangeHandler sAudioAudioVolumeGroupChangedHandler =
-            new AudioVolumeGroupChangeHandler();
 
     private static WeakReference<Context> sContext;
 
@@ -8761,9 +8760,13 @@
         }
     }
 
+    //====================================================================
+    // Notification of volume group changes
     /**
+     * Callback to receive updates on volume group changes, register using
+     * {@link AudioManager#registerVolumeGroupCallback(Executor, AudioVolumeCallback)}.
+     *
      * @hide
-     * Callback registered by client to be notified upon volume group change.
      */
     @SystemApi
     public abstract static class VolumeGroupCallback {
@@ -8774,35 +8777,70 @@
         public void onAudioVolumeGroupChanged(int group, int flags) {}
     }
 
-   /**
-    * @hide
-    * Register an audio volume group change listener.
-    * @param callback the {@link VolumeGroupCallback} to register
-    */
+    /**
+     * Register an audio volume group change listener.
+     *
+     * @param executor {@link Executor} to handle the callbacks
+     * @param callback the callback to receive the audio volume group changes
+     * @throws SecurityException if the caller doesn't have the required permission.
+     *
+     * @hide
+     */
     @SystemApi
-    public void registerVolumeGroupCallback(
-            @NonNull Executor executor,
+    @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING)
+    @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    public void registerVolumeGroupCallback(@NonNull Executor executor,
             @NonNull VolumeGroupCallback callback) {
-        Preconditions.checkNotNull(executor, "executor must not be null");
-        Preconditions.checkNotNull(callback, "volume group change cb must not be null");
-        sAudioAudioVolumeGroupChangedHandler.init();
-        // TODO: make use of executor
-        sAudioAudioVolumeGroupChangedHandler.registerListener(callback);
+        mVolumeChangedListenerMgr.addListener(executor, callback, "registerVolumeGroupCallback",
+                () -> new AudioVolumeChangeDispatcherStub());
     }
 
-   /**
-    * @hide
-    * Unregister an audio volume group change listener.
-    * @param callback the {@link VolumeGroupCallback} to unregister
-    */
+    /**
+     * Unregister an audio volume group change listener.
+     *
+     * @param callback the {@link VolumeGroupCallback} to unregister
+     *
+     * @hide
+     */
     @SystemApi
-    public void unregisterVolumeGroupCallback(
-            @NonNull VolumeGroupCallback callback) {
-        Preconditions.checkNotNull(callback, "volume group change cb must not be null");
-        sAudioAudioVolumeGroupChangedHandler.unregisterListener(callback);
+    @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING)
+    @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    public void unregisterVolumeGroupCallback(@NonNull VolumeGroupCallback callback) {
+        mVolumeChangedListenerMgr.removeListener(callback, "unregisterVolumeGroupCallback");
     }
 
     /**
+     * Manages the VolumeGroupCallback listeners and the AudioVolumeChangeDispatcherStub
+     */
+    private final CallbackUtil.LazyListenerManager<VolumeGroupCallback> mVolumeChangedListenerMgr =
+            new CallbackUtil.LazyListenerManager();
+
+    final class AudioVolumeChangeDispatcherStub extends IAudioVolumeChangeDispatcher.Stub
+            implements CallbackUtil.DispatcherStub {
+
+        @Override
+        public void register(boolean register) {
+            try {
+                if (register) {
+                    getService().registerAudioVolumeCallback(this);
+                } else {
+                    getService().unregisterAudioVolumeCallback(this);
+                }
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+
+        @Override
+        public void onAudioVolumeGroupChanged(int group, int flags) {
+            mVolumeChangedListenerMgr.callListeners((listener) ->
+                    listener.onAudioVolumeGroupChanged(group, flags));
+        }
+    }
+
+    //====================================================================
+
+    /**
      * Return if an asset contains haptic channels or not.
      *
      * @param context the {@link Context} to resolve the uri.
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index ad6f2e52..4906cd3 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2732,4 +2732,25 @@
      * @hide
      */
     public static native void triggerSystemPropertyUpdate(long handle);
+
+    /**
+     * Registers the given {@link INativeAudioVolumeGroupCallback} to native audioserver.
+     * @param callback to register
+     * @return {@link #SUCCESS} if successfully registered.
+     *
+     * @hide
+     */
+    public static native int registerAudioVolumeGroupCallback(
+            INativeAudioVolumeGroupCallback callback);
+
+    /**
+     * Unegisters the given {@link INativeAudioVolumeGroupCallback} from native audioserver
+     * previously registered via {@link #registerAudioVolumeGroupCallback}.
+     * @param callback to register
+     * @return {@link #SUCCESS} if successfully registered.
+     *
+     * @hide
+     */
+    public static native int unregisterAudioVolumeGroupCallback(
+            INativeAudioVolumeGroupCallback callback);
 }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 8aadb41..c505bce 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -65,6 +65,7 @@
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
 import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
 import android.media.projection.IMediaProjection;
 import android.net.Uri;
 import android.os.PersistableBundle;
@@ -446,6 +447,12 @@
 
     boolean isAudioServerRunning();
 
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher avc);
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    oneway void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher avc);
+
     int setUidDeviceAffinity(in IAudioPolicyCallback pcb, in int uid, in int[] deviceTypes,
              in String[] deviceAddresses);
 
diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
deleted file mode 100644
index 022cfee..0000000
--- a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.media.audiopolicy;
-
-import android.annotation.NonNull;
-import android.media.AudioManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
-
-import com.android.internal.util.Preconditions;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-/**
- * The AudioVolumeGroupChangeHandler handles AudioManager.OnAudioVolumeGroupChangedListener
- * callbacks posted from JNI
- *
- * TODO: Make use of Executor of callbacks.
- * @hide
- */
-public class AudioVolumeGroupChangeHandler {
-    private Handler mHandler;
-    private HandlerThread mHandlerThread;
-    private final ArrayList<AudioManager.VolumeGroupCallback> mListeners =
-            new ArrayList<AudioManager.VolumeGroupCallback>();
-
-    private static final String TAG = "AudioVolumeGroupChangeHandler";
-
-    private static final int AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED = 1000;
-    private static final int AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER = 4;
-
-    /**
-     * Accessed by native methods: JNI Callback context.
-     */
-    @SuppressWarnings("unused")
-    private long mJniCallback;
-
-    /**
-     * Initialization
-     */
-    public void init() {
-        synchronized (this) {
-            if (mHandler != null) {
-                return;
-            }
-            // create a new thread for our new event handler
-            mHandlerThread = new HandlerThread(TAG);
-            mHandlerThread.start();
-
-            if (mHandlerThread.getLooper() == null) {
-                mHandler = null;
-                return;
-            }
-            mHandler = new Handler(mHandlerThread.getLooper()) {
-                @Override
-                public void handleMessage(Message msg) {
-                    ArrayList<AudioManager.VolumeGroupCallback> listeners;
-                    synchronized (this) {
-                        if (msg.what == AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER) {
-                            listeners =
-                                    new ArrayList<AudioManager.VolumeGroupCallback>();
-                            if (mListeners.contains(msg.obj)) {
-                                listeners.add(
-                                        (AudioManager.VolumeGroupCallback) msg.obj);
-                            }
-                        } else {
-                            listeners = (ArrayList<AudioManager.VolumeGroupCallback>)
-                                    mListeners.clone();
-                        }
-                    }
-                    if (listeners.isEmpty()) {
-                        return;
-                    }
-
-                    switch (msg.what) {
-                        case AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED:
-                            for (int i = 0; i < listeners.size(); i++) {
-                                listeners.get(i).onAudioVolumeGroupChanged((int) msg.arg1,
-                                                                           (int) msg.arg2);
-                            }
-                            break;
-
-                        default:
-                            break;
-                    }
-                }
-            };
-            native_setup(new WeakReference<AudioVolumeGroupChangeHandler>(this));
-        }
-    }
-
-    private native void native_setup(Object moduleThis);
-
-    @Override
-    protected void finalize() {
-        native_finalize();
-        if (mHandlerThread.isAlive()) {
-            mHandlerThread.quit();
-        }
-    }
-    private native void native_finalize();
-
-   /**
-    * @param cb the {@link AudioManager.VolumeGroupCallback} to register
-    */
-    public void registerListener(@NonNull AudioManager.VolumeGroupCallback cb) {
-        Preconditions.checkNotNull(cb, "volume group callback shall not be null");
-        synchronized (this) {
-            mListeners.add(cb);
-        }
-        if (mHandler != null) {
-            Message m = mHandler.obtainMessage(
-                    AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER, 0, 0, cb);
-            mHandler.sendMessage(m);
-        }
-    }
-
-   /**
-    * @param cb the {@link AudioManager.VolumeGroupCallback} to unregister
-    */
-    public void unregisterListener(@NonNull AudioManager.VolumeGroupCallback cb) {
-        Preconditions.checkNotNull(cb, "volume group callback shall not be null");
-        synchronized (this) {
-            mListeners.remove(cb);
-        }
-    }
-
-    Handler handler() {
-        return mHandler;
-    }
-
-    @SuppressWarnings("unused")
-    private static void postEventFromNative(Object moduleRef,
-                                            int what, int arg1, int arg2, Object obj) {
-        AudioVolumeGroupChangeHandler eventHandler =
-                (AudioVolumeGroupChangeHandler) ((WeakReference) moduleRef).get();
-        if (eventHandler == null) {
-            return;
-        }
-
-        if (eventHandler != null) {
-            Handler handler = eventHandler.handler();
-            if (handler != null) {
-                Message m = handler.obtainMessage(what, arg1, arg2, obj);
-                // Do not remove previous messages, as we would lose notification of group changes
-                handler.sendMessage(m);
-            }
-        }
-    }
-}
diff --git a/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl
new file mode 100644
index 0000000..e6f9024
--- /dev/null
+++ b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl
@@ -0,0 +1,31 @@
+/* Copyright (C) 2025 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 android.media.audiopolicy;
+
+/**
+ * AIDL for the AudioService to signal audio volume groups changes
+ *
+ * {@hide}
+ */
+oneway interface IAudioVolumeChangeDispatcher {
+
+    /**
+     * Called when a volume group has been changed
+     * @param group id of the volume group that has changed.
+     * @param flags one or more flags to describe the volume change.
+     */
+    void onAudioVolumeGroupChanged(int group, int flags);
+}
diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java
index fccdba8..ece87a6 100644
--- a/media/java/android/media/quality/MediaQualityContract.java
+++ b/media/java/android/media/quality/MediaQualityContract.java
@@ -72,6 +72,43 @@
      */
     public static final String LEVEL_OFF = "level_off";
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(prefix = "COLOR_TEMP", value = {
+            COLOR_TEMP_USER,
+            COLOR_TEMP_COOL,
+            COLOR_TEMP_STANDARD,
+            COLOR_TEMP_WARM,
+            COLOR_TEMP_USER_HDR10PLUS,
+            COLOR_TEMP_COOL_HDR10PLUS,
+            COLOR_TEMP_STANDARD_HDR10PLUS,
+            COLOR_TEMP_WARM_HDR10PLUS,
+            COLOR_TEMP_FMMSDR,
+            COLOR_TEMP_FMMHDR,
+    })
+    public @interface ColorTempValue {}
+
+    /** @hide */
+    public static final String COLOR_TEMP_USER = "color_temp_user";
+    /** @hide */
+    public static final String COLOR_TEMP_COOL = "color_temp_cool";
+    /** @hide */
+    public static final String COLOR_TEMP_STANDARD = "color_temp_standard";
+    /** @hide */
+    public static final String COLOR_TEMP_WARM = "color_temp_warm";
+    /** @hide */
+    public static final String COLOR_TEMP_USER_HDR10PLUS = "color_temp_user_hdr10plus";
+    /** @hide */
+    public static final String COLOR_TEMP_COOL_HDR10PLUS = "color_temp_cool_hdr10plus";
+    /** @hide */
+    public static final String COLOR_TEMP_STANDARD_HDR10PLUS = "color_temp_standard_hdr10plus";
+    /** @hide */
+    public static final String COLOR_TEMP_WARM_HDR10PLUS = "color_temp_warm_hdr10plus";
+    /** @hide */
+    public static final String COLOR_TEMP_FMMSDR = "color_temp_fmmsdr";
+    /** @hide */
+    public static final String COLOR_TEMP_FMMHDR = "color_temp_fmmhdr";
+
 
     /**
      * @hide
@@ -82,7 +119,6 @@
         String PARAMETER_NAME = "_name";
         String PARAMETER_PACKAGE = "_package";
         String PARAMETER_INPUT_ID = "_input_id";
-        String VENDOR_PARAMETERS = "_vendor_parameters";
     }
 
     /**
diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml
index 5c911b1..466da7e 100644
--- a/media/tests/AudioPolicyTest/AndroidManifest.xml
+++ b/media/tests/AudioPolicyTest/AndroidManifest.xml
@@ -19,6 +19,7 @@
 
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
     <uses-permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME" />
 
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
deleted file mode 100644
index 82394a2..0000000
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2020 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.audiopolicytest;
-
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-
-import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
-import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.audiopolicy.AudioVolumeGroup;
-import android.media.audiopolicy.AudioVolumeGroupChangeHandler;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Presubmit
-@RunWith(AndroidJUnit4.class)
-public class AudioVolumeGroupChangeHandlerTest {
-    private static final String TAG = "AudioVolumeGroupChangeHandlerTest";
-
-    @Rule
-    public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
-
-    private AudioManager mAudioManager;
-
-    @Before
-    public void setUp() {
-        mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
-    }
-
-    @Test
-    public void testRegisterInvalidCallback() {
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-
-        assertThrows(NullPointerException.class, () -> {
-            AudioManager.VolumeGroupCallback nullCb = null;
-            audioAudioVolumeGroupChangedHandler.registerListener(nullCb);
-        });
-    }
-
-    @Test
-    public void testUnregisterInvalidCallback() {
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-
-        final AudioVolumeGroupCallbackHelper cb = new AudioVolumeGroupCallbackHelper();
-        audioAudioVolumeGroupChangedHandler.registerListener(cb);
-
-        assertThrows(NullPointerException.class, () -> {
-            AudioManager.VolumeGroupCallback nullCb = null;
-            audioAudioVolumeGroupChangedHandler.unregisterListener(nullCb);
-        });
-        audioAudioVolumeGroupChangedHandler.unregisterListener(cb);
-    }
-
-    @Test
-    public void testRegisterUnregisterCallback() {
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-        final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper();
-
-        // Should not assert, otherwise test will fail
-        audioAudioVolumeGroupChangedHandler.registerListener(validCb);
-
-        // Should not assert, otherwise test will fail
-        audioAudioVolumeGroupChangedHandler.unregisterListener(validCb);
-    }
-
-    @Test
-    public void testCallbackReceived() {
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-
-        final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper();
-        audioAudioVolumeGroupChangedHandler.registerListener(validCb);
-
-        List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
-        assertTrue(audioVolumeGroups.size() > 0);
-
-        try {
-            for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) {
-                int volumeGroupId = audioVolumeGroup.getId();
-
-                List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
-                // Set the volume per attributes (if valid) and wait the callback
-                if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
-                    // Some volume groups may not have valid attributes, used for internal
-                    // volume management like patch/rerouting
-                    // so bailing out strategy retrieval from attributes
-                    continue;
-                }
-                final AudioAttributes aa = avgAttributes.get(0);
-
-                int index = mAudioManager.getVolumeIndexForAttributes(aa);
-                int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa);
-                int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa);
-
-                final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax);
-
-                // Set the receiver to filter only the current group callback
-                validCb.setExpectedVolumeGroup(volumeGroupId);
-                mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/);
-                assertTrue(validCb.waitForExpectedVolumeGroupChanged(
-                        AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS));
-
-                final int readIndex = mAudioManager.getVolumeIndexForAttributes(aa);
-                assertEquals(readIndex, indexForAa);
-            }
-        } finally {
-            audioAudioVolumeGroupChangedHandler.unregisterListener(validCb);
-        }
-    }
-
-    @Test
-    public void testMultipleCallbackReceived() {
-
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-
-        final int callbackCount = 10;
-        final List<AudioVolumeGroupCallbackHelper> validCbs =
-                new ArrayList<AudioVolumeGroupCallbackHelper>();
-        for (int i = 0; i < callbackCount; i++) {
-            validCbs.add(new AudioVolumeGroupCallbackHelper());
-        }
-        for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
-            audioAudioVolumeGroupChangedHandler.registerListener(cb);
-        }
-
-        List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
-        assertTrue(audioVolumeGroups.size() > 0);
-
-        try {
-            for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) {
-                int volumeGroupId = audioVolumeGroup.getId();
-
-                List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
-                // Set the volume per attributes (if valid) and wait the callback
-                if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
-                    // Some volume groups may not have valid attributes, used for internal
-                    // volume management like patch/rerouting
-                    // so bailing out strategy retrieval from attributes
-                    continue;
-                }
-                AudioAttributes aa = avgAttributes.get(0);
-
-                int index = mAudioManager.getVolumeIndexForAttributes(aa);
-                int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa);
-                int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa);
-
-                final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax);
-
-                // Set the receiver to filter only the current group callback
-                for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
-                    cb.setExpectedVolumeGroup(volumeGroupId);
-                }
-                mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/);
-
-                for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
-                    assertTrue(cb.waitForExpectedVolumeGroupChanged(
-                            AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS));
-                }
-                int readIndex = mAudioManager.getVolumeIndexForAttributes(aa);
-                assertEquals(readIndex, indexForAa);
-            }
-        } finally {
-            for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
-                audioAudioVolumeGroupChangedHandler.unregisterListener(cb);
-            }
-        }
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index c4e7245..21d518a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -27,7 +27,6 @@
 import android.database.ContentObserver
 import android.os.Handler
 import android.provider.Settings
-import com.android.settingslib.flags.Flags
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.settingslib.notification.modes.ZenModesBackend
 import java.time.Duration
@@ -35,6 +34,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
@@ -72,7 +72,7 @@
     private val notificationManager: NotificationManager,
     private val backend: ZenModesBackend,
     private val contentResolver: ContentResolver,
-    val scope: CoroutineScope,
+    val applicationScope: CoroutineScope,
     val backgroundCoroutineContext: CoroutineContext,
     // This is nullable just to simplify testing, since SettingsLib doesn't have a good way
     // to create a fake handler.
@@ -104,7 +104,7 @@
                 awaitClose { context.unregisterReceiver(receiver) }
             }
             .flowOn(backgroundCoroutineContext)
-            .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
+            .shareIn(started = SharingStarted.WhileSubscribed(), scope = applicationScope)
     }
 
     override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
@@ -129,14 +129,11 @@
             .map { mapper(it) }
             .onStart { emit(mapper(null)) }
             .flowOn(backgroundCoroutineContext)
-            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), null)
 
     private val zenConfigChanged by lazy {
         if (android.app.Flags.modesUi()) {
             callbackFlow {
-                    // emit an initial value
-                    trySend(Unit)
-
                     val observer =
                         object : ContentObserver(backgroundHandler) {
                             override fun onChange(selfChange: Boolean) {
@@ -163,16 +160,18 @@
         }
     }
 
-    override val modes: Flow<List<ZenMode>> by lazy {
-        if (android.app.Flags.modesUi()) {
+    override val modes: StateFlow<List<ZenMode>> =
+        if (android.app.Flags.modesUi())
             zenConfigChanged
                 .map { backend.modes }
                 .distinctUntilChanged()
                 .flowOn(backgroundCoroutineContext)
-        } else {
-            flowOf(emptyList())
-        }
-    }
+                .stateIn(
+                    scope = applicationScope,
+                    started = SharingStarted.Eagerly,
+                    initialValue = backend.modes,
+                )
+        else MutableStateFlow<List<ZenMode>>(emptyList())
 
     /**
      * Gets the current list of [ZenMode] instances according to the backend.
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
index b364368..ec7baf6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
@@ -75,10 +75,14 @@
 
     private val testScope: TestScope = TestScope()
 
+    private val initialModes = listOf(TestModeBuilder().setId("Built-in").build())
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
+        `when`(zenModesBackend.modes).thenReturn(initialModes)
+
         underTest =
             ZenModeRepositoryImpl(
                 context,
@@ -151,8 +155,8 @@
     fun modesListEmitsOnSettingsChange() {
         testScope.runTest {
             val values = mutableListOf<List<ZenMode>>()
-            val modes1 = listOf(TestModeBuilder().setId("One").build())
-            `when`(zenModesBackend.modes).thenReturn(modes1)
+
+            // an initial list of modes is read when the stateflow is created
             underTest.modes.onEach { values.add(it) }.launchIn(backgroundScope)
             runCurrent()
 
@@ -172,7 +176,7 @@
             triggerZenModeSettingUpdate()
             runCurrent()
 
-            assertThat(values).containsExactly(modes1, modes2, modes3).inOrder()
+            assertThat(values).containsExactly(initialModes, modes2, modes3).inOrder()
         }
     }
 
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a4852d2..685c689 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -289,6 +289,15 @@
   }
 }
 
+flag {
+    name: "notification_skip_silent_updates"
+    namespace: "systemui"
+    description: "Do not notify HeadsUpManager for silent updates."
+    bug: "401068530"
+    metadata {
+       purpose: PURPOSE_BUGFIX
+    }
+}
 
 flag {
     name: "scene_container"
@@ -1856,10 +1865,11 @@
 }
 
 flag {
-  name: "shade_header_fonts"
+  name: "shade_header_font_update"
   namespace: "systemui"
   description: "Updates the fonts of the shade header"
-  bug: "393609724"
+  bug: "393609960"
+  is_fixed_read_only: true
 }
 
 flag {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt
new file mode 100644
index 0000000..468c3dc
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 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.systemui.qs.tiles.dialog
+
+import android.content.Context
+import android.media.MediaRouter
+import android.provider.Settings
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.app.MediaRouteDialogPresenter
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.domain.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.domain.actions.intentInputs
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class CastDetailsViewModelTest : SysuiTestCase() {
+    var inputHandler: FakeQSTileIntentUserInputHandler = FakeQSTileIntentUserInputHandler()
+    private var context: Context = mock()
+    private var mediaRouter: MediaRouter = mock()
+    private var selectedRoute: MediaRouter.RouteInfo = mock()
+
+    @Test
+    fun testClickOnSettingsButton() {
+        var viewModel = CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
+
+        viewModel.clickOnSettingsButton()
+
+        assertThat(inputHandler.handledInputs).hasSize(1)
+        val intentInput = inputHandler.intentInputs.last()
+        assertThat(intentInput.expandable).isNull()
+        assertThat(intentInput.intent.action).isEqualTo(Settings.ACTION_CAST_SETTINGS)
+    }
+
+    @Test
+    fun testShouldShowChooserDialog() {
+        context.stub {
+            on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter
+        }
+        mediaRouter.stub {
+            on { selectedRoute } doReturn selectedRoute
+        }
+
+        var viewModel =
+            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
+
+        assertThat(viewModel.shouldShowChooserDialog())
+            .isEqualTo(
+                MediaRouteDialogPresenter.shouldShowChooserDialog(
+                    context,
+                    MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY,
+                )
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 3ecf302..67af7a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.DozeParameters
@@ -45,6 +46,8 @@
 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
 import com.android.wm.shell.appzoomout.AppZoomOut
 import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -65,8 +68,6 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
-import java.util.Optional
-import java.util.function.Consumer
 
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
@@ -75,6 +76,7 @@
     private val kosmos = testKosmos()
 
     private val applicationScope = kosmos.testScope.backgroundScope
+    private val shadeDisplayRepository = kosmos.fakeShadeDisplaysRepository
     @Mock private lateinit var statusBarStateController: StatusBarStateController
     @Mock private lateinit var blurUtils: BlurUtils
     @Mock private lateinit var biometricUnlockController: BiometricUnlockController
@@ -135,7 +137,8 @@
                 windowRootViewBlurInteractor,
                 appZoomOutOptional,
                 applicationScope,
-                dumpManager
+                dumpManager,
+                { shadeDisplayRepository },
             )
         notificationShadeDepthController.shadeAnimation = shadeAnimation
         notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring
@@ -355,6 +358,36 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+    fun updateBlurCallback_shadeInExternalDisplay_doesSetZeroZoom() {
+        notificationShadeDepthController.onPanelExpansionChanged(
+            ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false)
+        )
+        notificationShadeDepthController.addListener(listener)
+        shadeDisplayRepository.setDisplayId(1) // not default display.
+
+        notificationShadeDepthController.updateBlurCallback.doFrame(0)
+
+        verify(wallpaperController).setNotificationShadeZoom(eq(0f))
+        verify(listener).onWallpaperZoomOutChanged(eq(0f))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+    fun updateBlurCallback_shadeInDefaultDisplay_doesNotSetZeroZoom() {
+        notificationShadeDepthController.onPanelExpansionChanged(
+            ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false)
+        )
+        notificationShadeDepthController.addListener(listener)
+        shadeDisplayRepository.setDisplayId(0) // shade is in default display
+
+        notificationShadeDepthController.updateBlurCallback.doFrame(0)
+
+        verify(wallpaperController).setNotificationShadeZoom(floatThat { it != 0f })
+        verify(listener).onWallpaperZoomOutChanged(floatThat { it != 0f })
+    }
+
+    @Test
     @DisableFlags(Flags.FLAG_NOTIFICATION_SHADE_BLUR)
     fun updateBlurCallback_setsOpaque_whenScrim() {
         scrimVisibilityCaptor.value.accept(ScrimController.OPAQUE)
@@ -488,10 +521,10 @@
     }
 
     private fun enableSplitShade() {
-        `when` (shadeModeInteractor.isSplitShade).thenReturn(true)
+        `when`(shadeModeInteractor.isSplitShade).thenReturn(true)
     }
 
     private fun disableSplitShade() {
-        `when` (shadeModeInteractor.isSplitShade).thenReturn(false)
+        `when`(shadeModeInteractor.isSplitShade).thenReturn(false)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 6c498c8..5ec9b60 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
@@ -799,8 +800,8 @@
             }
 
         private val PROMOTED_CONTENT_WITH_COLOR =
-            PromotedNotificationContentModel.Builder("notif")
-                .apply {
+            PromotedNotificationContentBuilder("notif")
+                .applyToShared {
                     this.colors =
                         PromotedNotificationContentModel.Colors(
                             backgroundColor = PROMOTED_BACKGROUND_COLOR,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index 7f8f5f4..9ce43a0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.testKosmos
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
@@ -420,7 +420,7 @@
             // WHEN the notif gets a new UID that starts as visible
             activityManagerRepository.fake.startingIsAppVisibleValue = true
             val newPromotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.shortCriticalText = "Arrived"
                 }
             val newPromotedContent = newPromotedContentBuilder.build()
@@ -452,6 +452,6 @@
 
     companion object {
         private const val UID = 885
-        private val PROMOTED_CONTENT = PromotedNotificationContentModel.Builder("notif1").build()
+        private val PROMOTED_CONTENT = PromotedNotificationContentBuilder("notif1").build()
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
index 0b9b297..202d5cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -36,7 +36,7 @@
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.addNotif
 import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.testKosmos
@@ -65,7 +65,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock<StatusBarIconView>(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -96,7 +96,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -115,7 +115,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -135,7 +135,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -158,12 +158,12 @@
                     activeNotificationModel(
                         key = "notif1",
                         statusBarChipIcon = firstIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                     ),
                     activeNotificationModel(
                         key = "notif2",
                         statusBarChipIcon = secondIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                     ),
                     activeNotificationModel(
                         key = "notif3",
@@ -195,7 +195,7 @@
                         key = "notif",
                         uid = uid,
                         statusBarChipIcon = mock<StatusBarIconView>(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                     )
                 )
             )
@@ -223,14 +223,14 @@
                         key = "promotedNormal",
                         statusBarChipIcon = mock(),
                         promotedContent =
-                            PromotedNotificationContentModel.Builder("promotedNormal").build(),
+                            PromotedNotificationContentBuilder("promotedNormal").build(),
                         callType = CallType.None,
                     ),
                     activeNotificationModel(
                         key = "promotedCall",
                         statusBarChipIcon = mock(),
                         promotedContent =
-                            PromotedNotificationContentModel.Builder("promotedCall").build(),
+                            PromotedNotificationContentBuilder("promotedCall").build(),
                         callType = CallType.Ongoing,
                     ),
                 )
@@ -256,7 +256,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -269,7 +269,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -282,7 +282,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = thirdIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -302,7 +302,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -325,7 +325,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -348,7 +348,7 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = firstIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             setNotifs(listOf(notif1))
             assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder()
@@ -359,7 +359,7 @@
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = secondIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             setNotifs(listOf(notif1, notif2))
 
@@ -380,7 +380,7 @@
 
             // WHEN notif1 gets an update
             val notif1NewPromotedContent =
-                PromotedNotificationContentModel.Builder("notif1").apply {
+                PromotedNotificationContentBuilder("notif1").applyToShared {
                     this.shortCriticalText = "Arrived"
                 }
             setNotifs(
@@ -426,8 +426,7 @@
                     key = notif1Info.key,
                     uid = notif1Info.uid,
                     statusBarChipIcon = notif1Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
                 )
             )
             activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false)
@@ -443,8 +442,7 @@
                     key = notif2Info.key,
                     uid = notif2Info.uid,
                     statusBarChipIcon = notif2Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
                 )
             )
             activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
@@ -482,16 +480,14 @@
                     key = notif1Info.key,
                     uid = notif1Info.uid,
                     statusBarChipIcon = notif1Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
                 )
             val notif2 =
                 activeNotificationModel(
                     key = notif2Info.key,
                     uid = notif2Info.uid,
                     statusBarChipIcon = notif2Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2))
             assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
@@ -537,16 +533,14 @@
                     key = notif1Info.key,
                     uid = notif1Info.uid,
                     statusBarChipIcon = notif1Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
                 )
             val notif2 =
                 activeNotificationModel(
                     key = notif2Info.key,
                     uid = notif2Info.uid,
                     statusBarChipIcon = notif2Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2))
             assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
@@ -567,8 +561,7 @@
                     key = notif3Info.key,
                     uid = notif3Info.uid,
                     statusBarChipIcon = notif3Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif3Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2, notif3))
 
@@ -597,8 +590,7 @@
                     key = notif1Info.key,
                     uid = notif1Info.uid,
                     statusBarChipIcon = notif1Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
                 )
             setNotifs(listOf(notif1))
 
@@ -609,8 +601,7 @@
                     key = notif2Info.key,
                     uid = notif2Info.uid,
                     statusBarChipIcon = notif2Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2))
 
@@ -637,7 +628,7 @@
 
             // WHEN notif2 gets an update
             val notif2NewPromotedContent =
-                PromotedNotificationContentModel.Builder("notif2").apply {
+                PromotedNotificationContentBuilder("notif2").applyToShared {
                     this.shortCriticalText = "Arrived"
                 }
             setNotifs(
@@ -662,8 +653,7 @@
                     key = notif3Info.key,
                     uid = notif3Info.uid,
                     statusBarChipIcon = notif3Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif3Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2, notif3))
 
@@ -710,8 +700,7 @@
                     activeNotificationModel(
                         key = "notif|uid1",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("notif|uid1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif|uid1").build(),
                     )
                 )
             )
@@ -725,8 +714,7 @@
                     activeNotificationModel(
                         key = "notif|uid2",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("notif|uid2").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif|uid2").build(),
                     )
                 )
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 8368fa6..eecdbbf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
@@ -101,7 +102,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -121,7 +122,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -142,7 +143,7 @@
                         key = "notif",
                         appName = "Fake App Name",
                         statusBarChipIcon = icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -172,7 +173,7 @@
                         key = notifKey,
                         appName = "Fake App Name",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder(notifKey).build(),
+                        promotedContent = PromotedNotificationContentBuilder(notifKey).build(),
                     )
                 )
             )
@@ -195,7 +196,7 @@
             val latest by collectLastValue(underTest.chips)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.colors =
                         PromotedNotificationContentModel.Colors(
                             backgroundColor = 56,
@@ -229,12 +230,12 @@
                     activeNotificationModel(
                         key = "notif1",
                         statusBarChipIcon = firstIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                     ),
                     activeNotificationModel(
                         key = "notif2",
                         statusBarChipIcon = secondIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                     ),
                     activeNotificationModel(
                         key = "notif3",
@@ -264,13 +265,12 @@
                     activeNotificationModel(
                         key = firstKey,
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder(firstKey).build(),
+                        promotedContent = PromotedNotificationContentBuilder(firstKey).build(),
                     ),
                     activeNotificationModel(
                         key = secondKey,
                         statusBarChipIcon = null,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder(secondKey).build(),
+                        promotedContent = PromotedNotificationContentBuilder(secondKey).build(),
                     ),
                     activeNotificationModel(
                         key = thirdKey,
@@ -294,7 +294,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.shortCriticalText = "Arrived"
                     this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
                 }
@@ -321,7 +321,7 @@
             val latest by collectLastValue(underTest.chips)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply { this.time = null }
+                PromotedNotificationContentBuilder("notif").applyToShared { this.time = null }
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -346,7 +346,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.wasPromotedAutomatically = true
                     this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
                 }
@@ -374,7 +374,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.wasPromotedAutomatically = false
                     this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
                 }
@@ -402,7 +402,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 13.minutes.inWholeMilliseconds)
                 }
 
@@ -430,7 +430,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 500)
                 }
 
@@ -458,7 +458,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime)
                 }
 
@@ -486,7 +486,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime - 2.minutes.inWholeMilliseconds)
                 }
 
@@ -515,7 +515,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 3.minutes.inWholeMilliseconds)
                 }
 
@@ -555,7 +555,7 @@
             val whenElapsed = currentElapsed - 1.minutes.inWholeMilliseconds
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time =
                         When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = false)
                 }
@@ -592,7 +592,7 @@
             val whenElapsed = currentElapsed + 10.minutes.inWholeMilliseconds
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time =
                         When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = true)
                 }
@@ -623,7 +623,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             setNotifs(
@@ -653,7 +653,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             setNotifs(
@@ -690,11 +690,11 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             val otherPromotedContentBuilder =
-                PromotedNotificationContentModel.Builder("other notif").apply {
+                PromotedNotificationContentBuilder("other notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             val icon = createStatusBarIconViewOrNull()
@@ -738,7 +738,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             setNotifs(
@@ -781,7 +781,7 @@
                     activeNotificationModel(
                         key,
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent = PromotedNotificationContentModel.Builder(key).build(),
+                        promotedContent = PromotedNotificationContentBuilder(key).build(),
                     )
                 )
             )
@@ -809,7 +809,7 @@
                     activeNotificationModel(
                         key,
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent = PromotedNotificationContentModel.Builder(key).build(),
+                        promotedContent = PromotedNotificationContentBuilder(key).build(),
                     )
                 )
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 608a84b..83b3c9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -63,7 +63,7 @@
 import com.android.systemui.statusbar.notification.data.repository.addNotif
 import com.android.systemui.statusbar.notification.data.repository.addNotifs
 import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization
@@ -358,7 +358,7 @@
             addOngoingCallState(key = "call")
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.shortCriticalText = "Some text here"
                 }
             activeNotificationListRepository.addNotif(
@@ -741,7 +741,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -765,7 +765,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -791,14 +791,12 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                 )
             )
@@ -822,14 +820,12 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                 )
             )
@@ -857,20 +853,17 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = thirdIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                 )
             )
@@ -896,26 +889,22 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = thirdIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "fourthNotif",
                         statusBarChipIcon = fourthIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("fourthNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(),
                     ),
                 )
             )
@@ -941,20 +930,17 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                 )
             )
@@ -973,26 +959,22 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "fourthNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("fourthNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(),
                     ),
                 )
             )
@@ -1016,14 +998,12 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                 )
             )
@@ -1050,20 +1030,17 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = thirdIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                 )
             )
@@ -1092,7 +1069,7 @@
                 activeNotificationModel(
                     key = "notif",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif").build(),
                 )
             )
 
@@ -1114,14 +1091,14 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             )
             activeNotificationListRepository.addNotif(
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -1143,14 +1120,14 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             )
             activeNotificationListRepository.addNotif(
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -1178,7 +1155,7 @@
                 activeNotificationModel(
                     key = "notif",
                     statusBarChipIcon = notifIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif").build(),
                 )
             )
             addOngoingCallState(key = callNotificationKey)
@@ -1189,7 +1166,7 @@
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = notifIcon2,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -1214,7 +1191,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = notifIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -1265,7 +1242,7 @@
                 activeNotificationModel(
                     key = "notif",
                     statusBarChipIcon = notifIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif").build(),
                 )
             )
 
@@ -1304,7 +1281,7 @@
                 activeNotificationModel(
                     key = "notif",
                     statusBarChipIcon = notifIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif").build(),
                 )
             )
             // And everything else hidden
@@ -1382,7 +1359,7 @@
                     activeNotificationModel(
                         key = "notif1",
                         statusBarChipIcon = notif1Icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                     )
                 )
             )
@@ -1436,7 +1413,7 @@
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = notif2Icon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index d3befa9..29bb29f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -170,7 +170,7 @@
             val promoted1 =
                 activeNotificationModel(
                     key = "notif1",
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             val notPromoted2 = activeNotificationModel(key = "notif2", promotedContent = null)
 
@@ -208,14 +208,14 @@
             val promoted1 =
                 activeNotificationModel(
                     key = "notif1",
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             val notPromoted2 = activeNotificationModel(key = "notif2", promotedContent = null)
             val notPromoted3 = activeNotificationModel(key = "notif3", promotedContent = null)
             val promoted4 =
                 activeNotificationModel(
                     key = "notif4",
-                    promotedContent = PromotedNotificationContentModel.Builder("notif4").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif4").build(),
                 )
 
             activeNotificationListRepository.activeNotifications.value =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
index 35b19c1..5c749e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
@@ -28,7 +28,8 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.byKey
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
@@ -130,7 +131,7 @@
             val promoted2 =
                 mockNotificationEntry(
                     "key2",
-                    promotedContent = PromotedNotificationContentModel.Builder("key2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("key2").build(),
                 )
 
             underTest.setRenderedList(listOf(notPromoted1, promoted2))
@@ -149,7 +150,7 @@
     private fun mockNotificationEntry(
         key: String,
         rank: Int = 0,
-        promotedContent: PromotedNotificationContentModel? = null,
+        promotedContent: PromotedNotificationContentModels? = null,
     ): NotificationEntry {
         val nBuilder = Notification.Builder(context, "a")
         val notification = nBuilder.build()
@@ -165,7 +166,7 @@
             whenever(this.representativeEntry).thenReturn(this)
             whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build())
             whenever(this.sbn).thenReturn(mockSbn)
-            whenever(this.promotedNotificationContentModel).thenReturn(promotedContent)
+            whenever(this.promotedNotificationContentModels).thenReturn(promotedContent)
         }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index ee698ae..41120a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -33,8 +33,10 @@
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor
 import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style.Base
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.byIsAmbient
 import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
 import com.android.systemui.statusbar.notification.shared.byIsPromoted
@@ -354,6 +356,6 @@
 private fun promotedContent(
     key: String,
     style: PromotedNotificationContentModel.Style,
-): PromotedNotificationContentModel {
-    return PromotedNotificationContentModel.Builder(key).apply { this.style = style }.build()
+): PromotedNotificationContentModels {
+    return PromotedNotificationContentBuilder(key).applyToShared { this.style = style }.build()
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
index 0ac944a..cc016b9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
@@ -33,18 +33,21 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.row.RowImageInflater
 import com.android.systemui.testKosmos
 import com.android.systemui.util.time.fakeSystemClock
 import com.android.systemui.util.time.systemClock
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.minutes
 import org.junit.Test
@@ -112,12 +115,43 @@
             setContentText(TEST_CONTENT_TEXT)
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.subText).isEqualTo(TEST_SUB_TEXT)
-        assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE)
-        assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT)
+        content.privateVersion.apply {
+            assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+            assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+            assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+        }
+
+        content.publicVersion.apply {
+            assertThat(subText).isNull()
+            assertThat(title).isNull()
+            assertThat(text).isNull()
+        }
+    }
+
+    @Test
+    @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
+    fun extractsContent_commonFields_noRedaction() {
+        val entry = createEntry {
+            setSubText(TEST_SUB_TEXT)
+            setContentTitle(TEST_CONTENT_TITLE)
+            setContentText(TEST_CONTENT_TEXT)
+        }
+
+        val content = requireContent(entry, redactionType = REDACTION_TYPE_NONE)
+
+        content.privateVersion.apply {
+            assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+            assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+            assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+        }
+
+        content.publicVersion.apply {
+            assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+            assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+            assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+        }
     }
 
     @Test
@@ -125,9 +159,9 @@
     fun extractContent_wasPromotedAutomatically_false() {
         val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content!!.wasPromotedAutomatically).isFalse()
+        assertThat(content.wasPromotedAutomatically).isFalse()
     }
 
     @Test
@@ -135,9 +169,9 @@
     fun extractContent_wasPromotedAutomatically_true() {
         val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, true) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content!!.wasPromotedAutomatically).isTrue()
+        assertThat(content.wasPromotedAutomatically).isTrue()
     }
 
     @Test
@@ -146,10 +180,9 @@
     fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() {
         val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content).isNotNull()
-        assertThat(content?.text).isNull()
+        assertThat(content.text).isNull()
     }
 
     @Test
@@ -161,10 +194,9 @@
     fun extractContent_apiFlagOn_shortCriticalTextExtracted() {
         val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content).isNotNull()
-        assertThat(content?.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
+        assertThat(content.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
     }
 
     @Test
@@ -176,10 +208,9 @@
     fun extractContent_noShortCriticalTextSet_textIsNull() {
         val entry = createEntry { setShortCriticalText(null) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content).isNotNull()
-        assertThat(content?.shortCriticalText).isNull()
+        assertThat(content.shortCriticalText).isNull()
     }
 
     @Test
@@ -379,17 +410,14 @@
             setWhen(providedCurrentTime)
         }
 
-        val content = extractContent(entry)
-
-        assertThat(content).isNotNull()
+        val content = requireContent(entry).privateVersion
 
         when (expected) {
-            ExpectedTime.Null -> assertThat(content?.time).isNull()
+            ExpectedTime.Null -> assertThat(content.time).isNull()
 
             ExpectedTime.Time -> {
-                val actual = content?.time as? When.Time
-                assertThat(actual).isNotNull()
-                assertThat(actual?.currentTimeMillis).isEqualTo(expectedCurrentTime)
+                val actual = assertNotNull(content.time as? When.Time)
+                assertThat(actual.currentTimeMillis).isEqualTo(expectedCurrentTime)
             }
 
             ExpectedTime.CountDown,
@@ -398,23 +426,24 @@
                     expectedCurrentTime + systemClock.elapsedRealtime() -
                         systemClock.currentTimeMillis()
 
-                val actual = content?.time as? When.Chronometer
-                assertThat(actual).isNotNull()
-                assertThat(actual?.elapsedRealtimeMillis).isEqualTo(expectedElapsedRealtime)
-                assertThat(actual?.isCountDown).isEqualTo(expected == ExpectedTime.CountDown)
+                val actual = assertNotNull(content.time as? When.Chronometer)
+                assertThat(actual.elapsedRealtimeMillis).isEqualTo(expectedElapsedRealtime)
+                assertThat(actual.isCountDown).isEqualTo(expected == ExpectedTime.CountDown)
             }
         }
     }
 
+    // TODO: Add tests for the style of the publicVersion once we implement that
+
     @Test
     @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
     fun extractContent_fromBaseStyle() {
         val entry = createEntry { setStyle(null) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.Base)
+        assertThat(content.privateVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
     }
 
     @Test
@@ -422,10 +451,10 @@
     fun extractContent_fromBigPictureStyle() {
         val entry = createEntry { setStyle(BigPictureStyle()) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.BigPicture)
+        assertThat(content.privateVersion.style).isEqualTo(Style.BigPicture)
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
     }
 
     @Test
@@ -442,12 +471,15 @@
             )
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.BigText)
-        assertThat(content?.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
-        assertThat(content?.text).isEqualTo(TEST_BIG_TEXT)
+        assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+        assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
+        assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
     }
 
     @Test
@@ -464,12 +496,15 @@
             )
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.BigText)
-        assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE)
-        assertThat(content?.text).isEqualTo(TEST_BIG_TEXT)
+        assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+        assertThat(content.privateVersion.title).isEqualTo(TEST_CONTENT_TITLE)
+        assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
     }
 
     @Test
@@ -486,12 +521,15 @@
             )
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.BigText)
-        assertThat(content?.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
-        assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT)
+        assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+        assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
+        assertThat(content.privateVersion.text).isEqualTo(TEST_CONTENT_TEXT)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
     }
 
     @Test
@@ -506,11 +544,14 @@
             )
         val entry = createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.Call)
-        assertThat(content?.title).isEqualTo(TEST_PERSON_NAME)
+        assertThat(content.privateVersion.style).isEqualTo(Style.Call)
+        assertThat(content.privateVersion.title).isEqualTo(TEST_PERSON_NAME)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
     }
 
     @Test
@@ -524,13 +565,17 @@
             setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75))
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.Progress)
-        assertThat(content?.newProgress).isNotNull()
-        assertThat(content?.newProgress?.progress).isEqualTo(75)
-        assertThat(content?.newProgress?.progressMax).isEqualTo(100)
+        assertThat(content.privateVersion.style).isEqualTo(Style.Progress)
+        val newProgress = assertNotNull(content.privateVersion.newProgress)
+        assertThat(newProgress.progress).isEqualTo(75)
+        assertThat(newProgress.progressMax).isEqualTo(100)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
+        assertThat(content.publicVersion.newProgress).isNull()
     }
 
     @Test
@@ -540,10 +585,11 @@
             setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON))
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.Ineligible)
+        assertThat(content.privateVersion.style).isEqualTo(Style.Ineligible)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Ineligible)
     }
 
     @Test
@@ -553,18 +599,13 @@
             setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false)
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
+        val oldProgress = assertNotNull(content.privateVersion.oldProgress)
 
-        val oldProgress = content?.oldProgress
-        assertThat(oldProgress).isNotNull()
-
-        assertThat(content).isNotNull()
-        assertThat(content?.oldProgress).isNotNull()
-        assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS)
-        assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX)
-        assertThat(content?.oldProgress?.isIndeterminate).isFalse()
+        assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
+        assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
+        assertThat(oldProgress.isIndeterminate).isFalse()
     }
 
     @Test
@@ -574,18 +615,25 @@
             setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true)
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
+        val oldProgress = assertNotNull(content.privateVersion.oldProgress)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.oldProgress).isNotNull()
-        assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS)
-        assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX)
-        assertThat(content?.oldProgress?.isIndeterminate).isTrue()
+        assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
+        assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
+        assertThat(oldProgress.isIndeterminate).isTrue()
     }
 
-    private fun extractContent(entry: NotificationEntry): PromotedNotificationContentModel? {
+    private fun requireContent(
+        entry: NotificationEntry,
+        redactionType: Int = REDACTION_TYPE_PUBLIC,
+    ): PromotedNotificationContentModels = assertNotNull(extractContent(entry, redactionType))
+
+    private fun extractContent(
+        entry: NotificationEntry,
+        redactionType: Int = REDACTION_TYPE_PUBLIC,
+    ): PromotedNotificationContentModels? {
         val recoveredBuilder = Notification.Builder(context, entry.sbn.notification)
-        return underTest.extractContent(entry, recoveredBuilder, imageModelProvider)
+        return underTest.extractContent(entry, recoveredBuilder, redactionType, imageModelProvider)
     }
 
     private fun createEntry(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
index 873ab5b..42c3f66 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
@@ -44,7 +44,7 @@
 import com.android.systemui.statusbar.notification.data.repository.addNotif
 import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
 import com.android.systemui.testKosmos
@@ -562,14 +562,14 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             )
             activeNotificationListRepository.addNotif(
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -608,14 +608,14 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             )
             activeNotificationListRepository.addNotif(
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -643,8 +643,7 @@
                 collectLastValue(underTest.aodPromotedNotification)
 
             // THEN the ron is first because the call has no content
-            assertThat(topPromotedNotificationContent?.identity?.key)
-                .isEqualTo("0|test_pkg|0|ron|0")
+            assertThat(topPromotedNotificationContent?.key).isEqualTo("0|test_pkg|0|ron|0")
         }
 
     @Test
@@ -663,8 +662,7 @@
                 collectLastValue(underTest.aodPromotedNotification)
 
             // THEN the call is the top notification
-            assertThat(topPromotedNotificationContent?.identity?.key)
-                .isEqualTo("0|test_pkg|0|call|0")
+            assertThat(topPromotedNotificationContent?.key).isEqualTo("0|test_pkg|0|call|0")
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index 99f2596..19b1046 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -67,11 +67,11 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
-import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
@@ -389,8 +389,8 @@
     @Test
     @DisableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME})
     public void testExtractsPromotedContent_notWhenBothFlagsDisabled() throws Exception {
-        final PromotedNotificationContentModel content =
-                new PromotedNotificationContentModel.Builder("key").build();
+        final PromotedNotificationContentModels content =
+                new PromotedNotificationContentBuilder("key").build();
         mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
 
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
@@ -401,43 +401,43 @@
     @Test
     @EnableFlags(PromotedNotificationUi.FLAG_NAME)
     @DisableFlags(StatusBarNotifChips.FLAG_NAME)
-    public void testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled()
+    public void testExtractsPromotedContent_whePromotedNotificationUiFlagEnabled()
             throws Exception {
-        final PromotedNotificationContentModel content =
-                new PromotedNotificationContentModel.Builder("key").build();
+        final PromotedNotificationContentModels content =
+                new PromotedNotificationContentBuilder("key").build();
         mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
 
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
 
         mPromotedNotificationContentExtractor.verifyOneExtractCall();
-        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
     }
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     @DisableFlags(PromotedNotificationUi.FLAG_NAME)
     public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception {
-        final PromotedNotificationContentModel content =
-                new PromotedNotificationContentModel.Builder("key").build();
+        final PromotedNotificationContentModels content =
+                new PromotedNotificationContentBuilder("key").build();
         mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
 
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
 
         mPromotedNotificationContentExtractor.verifyOneExtractCall();
-        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
     }
 
     @Test
     @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME})
     public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception {
-        final PromotedNotificationContentModel content =
-                new PromotedNotificationContentModel.Builder("key").build();
+        final PromotedNotificationContentModels content =
+                new PromotedNotificationContentBuilder("key").build();
         mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
 
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
 
         mPromotedNotificationContentExtractor.verifyOneExtractCall();
-        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
     }
 
     @Test
@@ -448,7 +448,7 @@
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
 
         mPromotedNotificationContentExtractor.verifyOneExtractCall();
-        assertNull(mRow.getEntry().getPromotedNotificationContentModel());
+        assertNull(mRow.getEntry().getPromotedNotificationContentModels());
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 063a04a..dcba3e4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -44,7 +44,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
@@ -456,7 +456,7 @@
     @Test
     @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
     fun testExtractsPromotedContent_notWhenBothFlagsDisabled() {
-        val content = PromotedNotificationContentModel.Builder("key").build()
+        val content = PromotedNotificationContentBuilder("key").build()
         promotedNotificationContentExtractor.resetForEntry(row.entry, content)
 
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
@@ -468,38 +468,38 @@
     @EnableFlags(PromotedNotificationUi.FLAG_NAME)
     @DisableFlags(StatusBarNotifChips.FLAG_NAME)
     fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() {
-        val content = PromotedNotificationContentModel.Builder("key").build()
+        val content = PromotedNotificationContentBuilder("key").build()
         promotedNotificationContentExtractor.resetForEntry(row.entry, content)
 
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
 
         promotedNotificationContentExtractor.verifyOneExtractCall()
-        Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+        Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
     }
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     @DisableFlags(PromotedNotificationUi.FLAG_NAME)
     fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() {
-        val content = PromotedNotificationContentModel.Builder("key").build()
+        val content = PromotedNotificationContentBuilder("key").build()
         promotedNotificationContentExtractor.resetForEntry(row.entry, content)
 
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
 
         promotedNotificationContentExtractor.verifyOneExtractCall()
-        Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+        Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
     }
 
     @Test
     @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
     fun testExtractsPromotedContent_whenBothFlagsEnabled() {
-        val content = PromotedNotificationContentModel.Builder("key").build()
+        val content = PromotedNotificationContentBuilder("key").build()
         promotedNotificationContentExtractor.resetForEntry(row.entry, content)
 
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
 
         promotedNotificationContentExtractor.verifyOneExtractCall()
-        Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+        Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
     }
 
     @Test
@@ -510,7 +510,7 @@
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
 
         promotedNotificationContentExtractor.verifyOneExtractCall()
-        Assert.assertNull(row.entry.promotedNotificationContentModel)
+        Assert.assertNull(row.entry.promotedNotificationContentModels)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index c58b4bc..18074d5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -40,7 +40,7 @@
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -170,7 +170,7 @@
     @Test
     fun interactorHasOngoingCallNotif_repoHasPromotedContent() =
         testScope.runTest {
-            val promotedContent = PromotedNotificationContentModel.Builder("ongoingNotif").build()
+            val promotedContent = PromotedNotificationContentBuilder("ongoingNotif").build()
             setNotifOnRepo(
                 activeNotificationModel(
                     key = "ongoingNotif",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index 84f1d5c..c071327a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
 import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
@@ -75,7 +75,7 @@
             val startTimeMs = 1000L
             val testIconView: StatusBarIconView = mock()
             val testIntent: PendingIntent = mock()
-            val testPromotedContent = PromotedNotificationContentModel.Builder(key).build()
+            val testPromotedContent = PromotedNotificationContentBuilder(key).build()
             addOngoingCallState(
                 key = key,
                 startTimeMs = startTimeMs,
@@ -106,7 +106,7 @@
             val startTimeMs = 1000L
             val testIconView: StatusBarIconView = mock()
             val testIntent: PendingIntent = mock()
-            val testPromotedContent = PromotedNotificationContentModel.Builder(key).build()
+            val testPromotedContent = PromotedNotificationContentBuilder(key).build()
             addOngoingCallState(
                 key = key,
                 startTimeMs = startTimeMs,
diff --git a/packages/SystemUI/res/layout/activity_rear_display_enabled.xml b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
index f900626..6b633e0 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
@@ -56,6 +56,7 @@
         android:gravity="center_horizontal" />
 
     <TextView
+        android:id="@+id/seekbar_instructions"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/rear_display_unfolded_front_screen_on_slide_to_cancel"
@@ -73,4 +74,13 @@
         android:background="@null"
         android:gravity="center_horizontal" />
 
+    <Button
+        android:id="@+id/cancel_button"
+        android:text="@string/cancel"
+        android:layout_width="@dimen/rear_display_animation_width_opened"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:visibility="gone"
+        style="@style/Widget.Dialog.Button.BorderButton"/>
+
 </LinearLayout>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
index ea73216..b8cd5be 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -208,4 +208,19 @@
         }
         mMatrix.postTranslate(translateX, translateY);
     }
+
+    /**
+     * A factory that returns a new instance of the {@link PreviewPositionHelper}.
+     * <p>{@link PreviewPositionHelper} is a stateful helper, and hence when using it in distinct
+     * scenarios, prefer fetching an object using this factory</p>
+     * <p>Additionally, helpful for injecting mocks in tests</p>
+     */
+    public static class PreviewPositionHelperFactory {
+        /**
+         * Returns a new {@link PreviewPositionHelper} for use in a distinct scenario.
+         */
+        public PreviewPositionHelper create() {
+            return new PreviewPositionHelper();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 8da06ac..099a7f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -140,6 +140,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor;
 import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -364,6 +365,7 @@
     private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
     private final Lazy<ShadeController> mShadeController;
     private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor;
+    private final Lazy<CommunalSettingsInteractor> mCommunalSettingsInteractor;
     /*
      * Records the user id on request to go away, for validation when WM calls back to start the
      * exit animation.
@@ -1567,6 +1569,7 @@
             KeyguardInteractor keyguardInteractor,
             KeyguardTransitionBootInteractor transitionBootInteractor,
             Lazy<CommunalSceneInteractor> communalSceneInteractor,
+            Lazy<CommunalSettingsInteractor> communalSettingsInteractor,
             WindowManagerOcclusionManager wmOcclusionManager) {
         mContext = context;
         mUserTracker = userTracker;
@@ -1609,6 +1612,7 @@
         mKeyguardInteractor = keyguardInteractor;
         mTransitionBootInteractor = transitionBootInteractor;
         mCommunalSceneInteractor = communalSceneInteractor;
+        mCommunalSettingsInteractor = communalSettingsInteractor;
 
         mStatusBarStateController = statusBarStateController;
         statusBarStateController.addCallback(this);
@@ -2429,9 +2433,18 @@
     private void doKeyguardLocked(Bundle options) {
         // If the power button behavior requests to open the glanceable hub.
         if (options != null && options.getBoolean(EXTRA_TRIGGER_HUB)) {
-            // Set the hub to show immediately when the SysUI window shows, then continue to lock
-            // the device.
-            mCommunalSceneInteractor.get().showHubFromPowerButton();
+            if (mCommunalSettingsInteractor.get().getAutoOpenEnabled().getValue()) {
+                // Set the hub to show immediately when the SysUI window shows, then continue to
+                // lock the device.
+                mCommunalSceneInteractor.get().showHubFromPowerButton();
+            } else {
+                // If the hub is not available, go to sleep instead of locking. This can happen
+                // because the power button behavior does not check all possible reasons the hub
+                // might be disabled.
+                mPM.goToSleep(android.os.SystemClock.uptimeMillis(),
+                        PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0);
+                return;
+            }
         }
 
         int currentUserId = mSelectedUserInteractor.getSelectedUserId();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
index ddccc5d..41d14b9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
@@ -20,7 +20,16 @@
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the keyguard wm state refactor flag state. */
+/**
+ * Helper for reading or using the keyguard_wm_state_refactor flag state.
+ *
+ * keyguard_wm_state_refactor works both with and without flexiglass (scene_container), but
+ * flexiglass requires keyguard_wm_state_refactor. For this reason, this class will return isEnabled
+ * if either keyguard_wm_state_refactor OR scene_container are enabled. This enables us to roll out
+ * keyguard_wm_state_refactor independently of scene_container, while also ensuring that
+ * scene_container rolling out ahead of keyguard_wm_state_refactor causes code gated by
+ * KeyguardWmStateRefactor to be enabled as well.
+ */
 @Suppress("NOTHING_TO_INLINE")
 object KeyguardWmStateRefactor {
     /** The aconfig flag name */
@@ -30,10 +39,9 @@
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
-    /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.keyguardWmStateRefactor()
+        get() = Flags.keyguardWmStateRefactor() || Flags.sceneContainer()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 6b1248b..1fe6eb9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -42,6 +42,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor;
 import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -182,6 +183,7 @@
             KeyguardInteractor keyguardInteractor,
             KeyguardTransitionBootInteractor transitionBootInteractor,
             Lazy<CommunalSceneInteractor> communalSceneInteractor,
+            Lazy<CommunalSettingsInteractor> communalSettingsInteractor,
             WindowManagerOcclusionManager windowManagerOcclusionManager) {
         return new KeyguardViewMediator(
                 context,
@@ -234,6 +236,7 @@
                 keyguardInteractor,
                 transitionBootInteractor,
                 communalSceneInteractor,
+                communalSettingsInteractor,
                 windowManagerOcclusionManager);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index 9312bca..a0458f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -64,7 +64,7 @@
 
     val shortcutsAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
-            duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+            duration = 200.milliseconds,
             onStep = alphaForAnimationStep,
             // Rapid swipes to bouncer, and may end up skipping intermediate values that would've
             // caused a complete fade out of lockscreen elements. Ensure it goes to 0f.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index b889c3e..9f04f69 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -19,6 +19,8 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.res.Configuration
+import android.graphics.Canvas
+import android.graphics.Path
 import android.graphics.PointF
 import android.graphics.Rect
 import android.os.Bundle
@@ -125,7 +127,6 @@
 import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
 import com.android.systemui.qs.composefragment.ui.GridAnchor
 import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams
-import com.android.systemui.qs.composefragment.ui.notificationScrimClip
 import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
 import com.android.systemui.qs.composefragment.ui.toEditMode
 import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
@@ -241,7 +242,7 @@
             FrameLayoutTouchPassthrough(
                 context,
                 { notificationScrimClippingParams.isEnabled },
-                { notificationScrimClippingParams.params.top },
+                snapshotFlow { notificationScrimClippingParams.params },
                 // Only allow scrolling when we are fully expanded. That way, we don't intercept
                 // swipes in lockscreen (when somehow QS is receiving touches).
                 { (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing },
@@ -276,11 +277,6 @@
                                     }
                                 }
                                 .graphicsLayer { alpha = viewModel.viewAlpha }
-                                .thenIf(notificationScrimClippingParams.isEnabled) {
-                                    Modifier.notificationScrimClip {
-                                        notificationScrimClippingParams.params
-                                    }
-                                }
                                 .thenIf(!Flags.notificationShadeBlur()) {
                                     Modifier.offset {
                                         IntOffset(
@@ -1061,17 +1057,75 @@
 private class FrameLayoutTouchPassthrough(
     context: Context,
     private val clippingEnabledProvider: () -> Boolean,
-    private val clippingTopProvider: () -> Int,
+    private val clippingParams: Flow<NotificationScrimClipParams>,
     private val canScrollForwardQs: () -> Boolean,
     private val emitMotionEventForFalsing: () -> Unit,
 ) : FrameLayout(context) {
+
+    init {
+        repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                clippingParams.collect { currentClipParams = it }
+            }
+        }
+    }
+
+    private val currentClippingPath = Path()
+    private var lastWidth = -1
+        set(value) {
+            if (field != value) {
+                field = value
+                updateClippingPath()
+            }
+        }
+
+    private var currentClipParams = NotificationScrimClipParams()
+        set(value) {
+            if (field != value) {
+                field = value
+                updateClippingPath()
+            }
+        }
+
+    private fun updateClippingPath() {
+        currentClippingPath.rewind()
+        if (clippingEnabledProvider()) {
+            val right = width + currentClipParams.rightInset
+            val left = -currentClipParams.leftInset
+            val top = currentClipParams.top
+            val bottom = currentClipParams.bottom
+            currentClippingPath.addRoundRect(
+                left.toFloat(),
+                top.toFloat(),
+                right.toFloat(),
+                bottom.toFloat(),
+                currentClipParams.radius.toFloat(),
+                currentClipParams.radius.toFloat(),
+                Path.Direction.CW,
+            )
+        }
+        invalidate()
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        lastWidth = right - left
+    }
+
+    override fun dispatchDraw(canvas: Canvas) {
+        if (!currentClippingPath.isEmpty) {
+            canvas.clipOutPath(currentClippingPath)
+        }
+        super.dispatchDraw(canvas)
+    }
+
     override fun isTransformedTouchPointInView(
         x: Float,
         y: Float,
         child: View?,
         outLocalPoint: PointF?,
     ): Boolean {
-        return if (clippingEnabledProvider() && y + translationY > clippingTopProvider()) {
+        return if (clippingEnabledProvider() && y + translationY > currentClipParams.top) {
             false
         } else {
             super.isTransformedTouchPointInView(x, y, child, outLocalPoint)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
deleted file mode 100644
index 3049a40..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.systemui.qs.composefragment.ui
-
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.CornerRadius
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.ClipOp
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.CompositingStrategy
-import androidx.compose.ui.graphics.drawscope.clipRect
-import androidx.compose.ui.graphics.graphicsLayer
-
-/**
- * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out
- * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)`
- * from the QS container.
- */
-fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier {
-    return this.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
-        .drawWithContent {
-            drawContent()
-            val params = clipParams()
-            val left = -params.leftInset.toFloat()
-            val right = size.width + params.rightInset.toFloat()
-            val top = params.top.toFloat()
-            val bottom = params.bottom.toFloat()
-            val clipSize = Size(right - left, bottom - top)
-            if (!clipSize.isEmpty()) {
-                clipRect {
-                    drawRoundRect(
-                        color = Color.Black,
-                        cornerRadius = CornerRadius(params.radius.toFloat()),
-                        blendMode = BlendMode.Clear,
-                        topLeft = Offset(left, top),
-                        size = Size(right - left, bottom - top),
-                    )
-                }
-            }
-        }
-}
-
-/** Params for [notificationScrimClip]. */
-data class NotificationScrimClipParams(
-    val top: Int = 0,
-    val bottom: Int = 0,
-    val leftInset: Int = 0,
-    val rightInset: Int = 0,
-    val radius: Int = 0,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt
new file mode 100644
index 0000000..db320d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.systemui.qs.composefragment.ui
+
+/** Params for [notificationScrimClip]. */
+data class NotificationScrimClipParams(
+    val top: Int = 0,
+    val bottom: Int = 0,
+    val leftInset: Int = 0,
+    val rightInset: Int = 0,
+    val radius: Int = 0,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
index 263ef09e..5d4a774 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
@@ -19,14 +19,18 @@
 import android.content.Context
 import android.hardware.devicestate.DeviceStateManager
 import android.hardware.devicestate.feature.flags.Flags
+import android.os.Handler
+import android.view.accessibility.AccessibilityManager
 import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.display.domain.interactor.RearDisplayStateInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
@@ -52,6 +56,8 @@
     private val rearDisplayInnerDialogDelegateFactory: RearDisplayInnerDialogDelegate.Factory,
     @Application private val scope: CoroutineScope,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val accessibilityManager: AccessibilityManager,
+    @Background private val handler: Handler,
 ) : CoreStartable, AutoCloseable {
 
     companion object {
@@ -77,6 +83,12 @@
     override fun start() {
         if (Flags.deviceStateRdmV2()) {
             var dialog: SystemUIDialog? = null
+            var touchExplorationEnabled = AtomicBoolean(false)
+
+            accessibilityManager.addTouchExplorationStateChangeListener(
+                { enabled -> touchExplorationEnabled.set(enabled) },
+                handler,
+            )
 
             keyguardUpdateMonitor.registerCallback(keyguardCallback)
 
@@ -99,6 +111,7 @@
                                             rearDisplayInnerDialogDelegateFactory.create(
                                                 rearDisplayContext,
                                                 deviceStateManager::cancelStateRequest,
+                                                touchExplorationEnabled.get(),
                                             )
                                         dialog = delegate.createDialog().apply { show() }
                                     }
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
index f5facf4..96f1bd2 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
@@ -20,7 +20,10 @@
 import android.content.Context
 import android.os.Bundle
 import android.view.MotionEvent
+import android.view.View
+import android.widget.Button
 import android.widget.SeekBar
+import android.widget.TextView
 import com.android.systemui.haptics.slider.HapticSlider
 import com.android.systemui.haptics.slider.HapticSliderPlugin
 import com.android.systemui.haptics.slider.HapticSliderViewBinder
@@ -45,6 +48,7 @@
 internal constructor(
     private val systemUIDialogFactory: SystemUIDialog.Factory,
     @Assisted private val rearDisplayContext: Context,
+    @Assisted private val touchExplorationEnabled: Boolean,
     private val vibratorHelper: VibratorHelper,
     private val msdlPlayer: MSDLPlayer,
     private val systemClock: SystemClock,
@@ -82,6 +86,7 @@
         fun create(
             rearDisplayContext: Context,
             onCanceledRunnable: Runnable,
+            touchExplorationEnabled: Boolean,
         ): RearDisplayInnerDialogDelegate
     }
 
@@ -95,11 +100,32 @@
 
     @SuppressLint("ClickableViewAccessibility")
     override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+
         dialog.apply {
             setContentView(R.layout.activity_rear_display_enabled)
             setCanceledOnTouchOutside(false)
 
+            requireViewById<Button>(R.id.cancel_button).let { it ->
+                if (!touchExplorationEnabled) {
+                    return@let
+                }
+
+                it.visibility = View.VISIBLE
+                it.setOnClickListener { onCanceledRunnable.run() }
+            }
+
+            requireViewById<TextView>(R.id.seekbar_instructions).let { it ->
+                if (touchExplorationEnabled) {
+                    it.visibility = View.GONE
+                }
+            }
+
             requireViewById<SeekBar>(R.id.seekbar).let { it ->
+                if (touchExplorationEnabled) {
+                    it.visibility = View.GONE
+                    return@let
+                }
+
                 // Create and bind the HapticSliderPlugin
                 val hapticSliderPlugin =
                     HapticSliderPlugin(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index fce5a16..e292bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -24,6 +24,7 @@
 import android.util.Log
 import android.util.MathUtils
 import android.view.Choreographer
+import android.view.Display
 import android.view.View
 import androidx.annotation.VisibleForTesting
 import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -42,7 +43,9 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
 import com.android.systemui.statusbar.phone.DozeParameters
@@ -52,6 +55,7 @@
 import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor
 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
 import com.android.wm.shell.appzoomout.AppZoomOut
+import dagger.Lazy
 import java.io.PrintWriter
 import java.util.Optional
 import javax.inject.Inject
@@ -83,6 +87,7 @@
     private val appZoomOutOptional: Optional<AppZoomOut>,
     @Application private val applicationScope: CoroutineScope,
     dumpManager: DumpManager,
+    private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>,
 ) : ShadeExpansionListener, Dumpable {
     companion object {
         private const val WAKE_UP_ANIMATION_ENABLED = true
@@ -228,6 +233,14 @@
 
     private data class WakeAndUnlockBlurData(val radius: Float, val useZoom: Boolean = true)
 
+    private val isShadeOnDefaultDisplay: Boolean
+        get() =
+            if (ShadeWindowGoesAround.isEnabled) {
+                shadeDisplaysRepository.get().displayId.value == Display.DEFAULT_DISPLAY
+            } else {
+                true
+            }
+
     /** Blur radius of the wake and unlock animation on this frame, and whether to zoom out. */
     private var wakeAndUnlockBlurData = WakeAndUnlockBlurData(0f)
         set(value) {
@@ -265,9 +278,14 @@
         var blur = shadeRadius.toInt()
         // If the blur comes from waking up, we don't want to zoom out the background
         val zoomOut =
-            if (shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom)
-                blurRadiusToZoomOut(blurRadius = shadeRadius)
-            else 0f
+            when {
+                // When the shade is in another display, we don't want to zoom out the background.
+                // Only the default display is supported right now.
+                !isShadeOnDefaultDisplay -> 0f
+                shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom ->
+                    blurRadiusToZoomOut(blurRadius = shadeRadius)
+                else -> 0f
+            }
         // Make blur be 0 if it is necessary to stop blur effect.
         if (scrimsVisible) {
             if (!Flags.notificationShadeBlur()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
index 1f2079d..356731c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.chips.notification.domain.model
 
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 
 /** Modeling all the data needed to render a status bar notification chip. */
 data class NotificationChipModel(
@@ -25,7 +25,7 @@
     /** The user-readable name of the app that posted this notification. */
     val appName: String,
     val statusBarChipIconView: StatusBarIconView?,
-    val promotedContent: PromotedNotificationContentModel,
+    val promotedContent: PromotedNotificationContentModels,
     /** The time when the notification first appeared as promoted. */
     val creationTime: Long,
     /** True if the app managing this notification is currently visible to the user. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index b303751..dfbd12d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -72,6 +72,8 @@
         headsUpState: TopPinnedState
     ): OngoingActivityChipModel.Active {
         StatusBarNotifChips.unsafeAssertInNewMode()
+        // Chips are never shown when locked, so it's safe to use the version with sensitive content
+        val chipContent = promotedContent.privateVersion
         val contentDescription = getContentDescription(this.appName)
         val icon =
             if (this.statusBarChipIconView != null) {
@@ -123,21 +125,18 @@
             )
         }
 
-        if (this.promotedContent.shortCriticalText != null) {
+        if (chipContent.shortCriticalText != null) {
             return OngoingActivityChipModel.Active.Text(
                 key = this.key,
                 icon = icon,
                 colors = colors,
-                text = this.promotedContent.shortCriticalText,
+                text = chipContent.shortCriticalText,
                 onClickListenerLegacy = onClickListenerLegacy,
                 clickBehavior = clickBehavior,
             )
         }
 
-        if (
-            Flags.promoteNotificationsAutomatically() &&
-                this.promotedContent.wasPromotedAutomatically
-        ) {
+        if (Flags.promoteNotificationsAutomatically() && chipContent.wasPromotedAutomatically) {
             // When we're promoting notifications automatically, the `when` time set on the
             // notification will likely just be set to the current time, which would cause the chip
             // to always show "now". We don't want early testers to get that experience since it's
@@ -151,7 +150,7 @@
             )
         }
 
-        if (this.promotedContent.time == null) {
+        if (chipContent.time == null) {
             return OngoingActivityChipModel.Active.IconOnly(
                 key = this.key,
                 icon = icon,
@@ -161,17 +160,17 @@
             )
         }
 
-        when (this.promotedContent.time) {
+        when (chipContent.time) {
             is PromotedNotificationContentModel.When.Time -> {
                 return if (
-                    this.promotedContent.time.currentTimeMillis >=
+                    chipContent.time.currentTimeMillis >=
                         systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS
                 ) {
                     OngoingActivityChipModel.Active.ShortTimeDelta(
                         key = this.key,
                         icon = icon,
                         colors = colors,
-                        time = this.promotedContent.time.currentTimeMillis,
+                        time = chipContent.time.currentTimeMillis,
                         onClickListenerLegacy = onClickListenerLegacy,
                         clickBehavior = clickBehavior,
                     )
@@ -198,8 +197,8 @@
                     key = this.key,
                     icon = icon,
                     colors = colors,
-                    startTimeMs = this.promotedContent.time.elapsedRealtimeMillis,
-                    isEventInFuture = this.promotedContent.time.isCountDown,
+                    startTimeMs = chipContent.time.elapsedRealtimeMillis,
+                    isEventInFuture = chipContent.time.isCountDown,
                     onClickListenerLegacy = onClickListenerLegacy,
                     clickBehavior = clickBehavior,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 4558017..b5ab092 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -68,6 +68,7 @@
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
 import com.android.systemui.statusbar.notification.icon.IconPack;
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
@@ -198,7 +199,7 @@
 
     // TODO(b/377565433): Move into NotificationContentModel during/after
     //  NotificationRowContentBinderRefactor.
-    private PromotedNotificationContentModel mPromotedNotificationContentModel;
+    private PromotedNotificationContentModels mPromotedNotificationContentModels;
 
     /**
      * True if both
@@ -1106,9 +1107,9 @@
      * Gets the content needed to render this notification as a promoted notification on various
      * surfaces (like status bar chips and AOD).
      */
-    public PromotedNotificationContentModel getPromotedNotificationContentModel() {
+    public PromotedNotificationContentModels getPromotedNotificationContentModels() {
         if (PromotedNotificationContentModel.featureFlagEnabled()) {
-            return mPromotedNotificationContentModel;
+            return mPromotedNotificationContentModels;
         } else {
             Log.wtf(TAG, "getting promoted content without feature flag enabled", new Throwable());
             return null;
@@ -1127,10 +1128,10 @@
      * Sets the content needed to render this notification as a promoted notification on various
      * surfaces (like status bar chips and AOD).
      */
-    public void setPromotedNotificationContentModel(
-            @Nullable PromotedNotificationContentModel promotedNotificationContentModel) {
+    public void setPromotedNotificationContentModels(
+            @Nullable PromotedNotificationContentModels promotedNotificationContentModels) {
         if (PromotedNotificationContentModel.featureFlagEnabled()) {
-            this.mPromotedNotificationContentModel = promotedNotificationContentModel;
+            this.mPromotedNotificationContentModels = promotedNotificationContentModels;
         } else {
             Log.wtf(TAG, "setting promoted content without feature flag enabled", new Throwable());
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index a0eab43..26b86f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import com.android.systemui.Flags.notificationSkipSilentUpdates
+
 import android.app.Notification
 import android.app.Notification.GROUP_ALERT_SUMMARY
 import android.util.ArrayMap
@@ -465,15 +467,32 @@
                             }
                         hunMutator.updateNotification(posted.key, pinnedStatus)
                     }
-                } else {
+                } else { // shouldHeadsUpEver = false
                     if (posted.isHeadsUpEntry) {
-                        // We don't want this to be interrupting anymore, let's remove it
-                        // If the notification is pinned by the user, the only way a user can un-pin
-                        // it is by tapping the status bar notification chip. Since that's a clear
-                        // user action, we should remove the HUN immediately instead of waiting for
-                        // any sort of minimum timeout.
-                        val shouldRemoveImmediately = posted.isPinnedByUser
-                        hunMutator.removeNotification(posted.key, shouldRemoveImmediately)
+                        if (notificationSkipSilentUpdates()) {
+                            if (posted.isPinnedByUser) {
+                                // We don't want this to be interrupting anymore, let's remove it
+                                // If the notification is pinned by the user, the only way a user
+                                // can un-pin it by tapping the status bar notification chip. Since
+                                // that's a clear user action, we should remove the HUN immediately
+                                // instead of waiting for any sort of minimum timeout.
+                                // TODO(b/401068530) Ensure that status bar chip HUNs are not
+                                //  removed for silent update
+                                hunMutator.removeNotification(posted.key,
+                                    /* releaseImmediately= */ true)
+                            } else {
+                                // Do NOT remove HUN for non-user update.
+                                // Let the HUN show for its remaining duration.
+                            }
+                        } else {
+                            // We don't want this to be interrupting anymore, let's remove it
+                            // If the notification is pinned by the user, the only way a user can
+                            // un-pin it is by tapping the status bar notification chip. Since
+                            // that's a clear user action, we should remove the HUN immediately
+                            // instead of waiting for any sort of minimum timeout.
+                            val shouldRemoveImmediately = posted.isPinnedByUser
+                            hunMutator.removeNotification(posted.key, shouldRemoveImmediately)
+                        }
                     } else {
                         // Don't let the bind finish
                         cancelHeadsUpBind(posted.entry)
@@ -573,24 +592,34 @@
                                 isBinding = isBinding,
                             )
                     }
-                // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so
-                // that
-                // work can be done before the ShadeListBuilder is run. This prevents re-entrant
-                // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
-                if (posted?.shouldHeadsUpEver == false) {
-                    if (posted.isHeadsUpEntry) {
-                        // We don't want this to be interrupting anymore, let's remove it
-                        mHeadsUpManager.removeNotification(
-                            posted.key,
-                            /* removeImmediately= */ false,
-                            "onEntryUpdated",
-                        )
-                    } else if (posted.isBinding) {
+                if (notificationSkipSilentUpdates()) {
+                    // TODO(b/403703828) Move canceling to OnBeforeFinalizeFilter, since we are not
+                    //  removing from HeadsUpManager and don't need to deal with re-entrant behavior
+                    //  between HeadsUpCoordinator, HeadsUpManager, and VisualStabilityManager.
+                    if (posted?.shouldHeadsUpEver == false
+                        && !posted.isHeadsUpEntry && posted.isBinding) {
                         // Don't let the bind finish
                         cancelHeadsUpBind(posted.entry)
                     }
+                } else {
+                    // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter,
+                    // so that work can be done before the ShadeListBuilder is run. This prevents
+                    // re-entrant behavior between this Coordinator, HeadsUpManager, and
+                    // VisualStabilityManager.
+                    if (posted?.shouldHeadsUpEver == false) {
+                        if (posted.isHeadsUpEntry) {
+                            // We don't want this to be interrupting anymore, let's remove it
+                            mHeadsUpManager.removeNotification(
+                                posted.key,
+                                /* removeImmediately= */ false,
+                                "onEntryUpdated",
+                            )
+                        } else if (posted.isBinding) {
+                            // Don't let the bind finish
+                            cancelHeadsUpBind(posted.entry)
+                        }
+                    }
                 }
-
                 // Update last updated time for this entry
                 setUpdateTime(entry, mSystemClock.currentTimeMillis())
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 1e5aa01..6042bff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -83,7 +83,6 @@
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl;
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
-import com.android.systemui.statusbar.notification.row.NotificationActionClickManager;
 import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory;
 import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -325,7 +324,7 @@
         if (PromotedNotificationContentModel.featureFlagEnabled()) {
             return implProvider.get();
         } else {
-            return (entry, recoveredBuilder, imageModelProvider) -> null;
+            return (entry, recoveredBuilder, redactionType, imageModelProvider) -> null;
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index 2f9d86b..e7cc342 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
@@ -141,7 +142,7 @@
     private fun NotificationEntry.toModel(): ActiveNotificationModel {
         val promotedContent =
             if (PromotedNotificationContentModel.featureFlagEnabled()) {
-                promotedNotificationContentModel
+                promotedNotificationContentModels
             } else {
                 null
             }
@@ -199,7 +200,7 @@
     isGroupSummary: Boolean,
     bucket: Int,
     callType: CallType,
-    promotedContent: PromotedNotificationContentModel?,
+    promotedContent: PromotedNotificationContentModels?,
 ): ActiveNotificationModel {
     return individuals[key]?.takeIf {
         it.isCurrent(
@@ -281,7 +282,7 @@
     isGroupSummary: Boolean,
     bucket: Int,
     callType: CallType,
-    promotedContent: PromotedNotificationContentModel?,
+    promotedContent: PromotedNotificationContentModels?,
 ): Boolean {
     return when {
         key != this.key -> false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index d09546f..3caaf54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -67,6 +67,7 @@
     private FooterViewButton mSettingsButton;
     private FooterViewButton mHistoryButton;
     private boolean mShouldBeHidden;
+    private boolean mIsBlurSupported;
 
     // Footer label
     private TextView mSeenNotifsFooterTextView;
@@ -390,15 +391,20 @@
 
         if (!notificationFooterBackgroundTintOptimization()) {
             if (notificationShadeBlur()) {
-                Color backgroundColor = Color.valueOf(
-                        SurfaceEffectColors.surfaceEffect1(getContext()));
-                scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
-                // Apply alpha on background drawables.
-                int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
-                clearAllBg.setAlpha(backgroundAlpha);
-                settingsBg.setAlpha(backgroundAlpha);
-                if (historyBg != null) {
-                    historyBg.setAlpha(backgroundAlpha);
+                if (mIsBlurSupported) {
+                    Color backgroundColor = Color.valueOf(
+                            SurfaceEffectColors.surfaceEffect1(getContext()));
+                    scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
+                    // Apply alpha on background drawables.
+                    int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
+                    clearAllBg.setAlpha(backgroundAlpha);
+                    settingsBg.setAlpha(backgroundAlpha);
+                    if (historyBg != null) {
+                        historyBg.setAlpha(backgroundAlpha);
+                    }
+                } else {
+                    scHigh = mContext.getColor(
+                            com.android.internal.R.color.materialColorSurfaceContainer);
                 }
             } else {
                 scHigh = mContext.getColor(
@@ -438,6 +444,16 @@
         }
     }
 
+    public void setIsBlurSupported(boolean isBlurSupported) {
+        if (notificationShadeBlur()) {
+            if (mIsBlurSupported == isBlurSupported) {
+                return;
+            }
+            mIsBlurSupported = isBlurSupported;
+            updateColors();
+        }
+    }
+
     @Override
     @NonNull
     public ExpandableViewState createExpandableViewState() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 3383ce9..3213754 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -20,6 +20,7 @@
 import androidx.lifecycle.lifecycleScope
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags.notificationShadeBlur
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.NotificationActivityStarter
 import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
@@ -81,6 +82,14 @@
             launch { bindHistoryButton(footer, viewModel, notificationActivityStarter) }
         }
         launch { bindMessage(footer, viewModel) }
+
+        if (notificationShadeBlur()) {
+            launch {
+                viewModel.isBlurSupported.collect { supported ->
+                    footer.setIsBlurSupported(supported)
+                }
+            }
+        }
     }
 
     private suspend fun bindClearAllButton(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index c895c419..c1fc72c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.util.ui.AnimatableEvent
 import com.android.systemui.util.ui.AnimatedValue
 import com.android.systemui.util.ui.toAnimatedValueFlow
+import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.flow.Flow
@@ -48,6 +49,7 @@
     notificationSettingsInteractor: NotificationSettingsInteractor,
     seenNotificationsInteractor: SeenNotificationsInteractor,
     shadeInteractor: ShadeInteractor,
+    windowRootViewBlurInteractor: WindowRootViewBlurInteractor,
 ) {
     /** A message to show instead of the footer buttons. */
     val message: FooterMessageViewModel =
@@ -119,6 +121,8 @@
         }
     }
 
+    val isBlurSupported = windowRootViewBlurInteractor.isBlurCurrentlySupported
+
     private val manageOrHistoryButtonText: Flow<Int> =
         notificationSettingsInteractor.isNotificationHistoryEnabled.map { shouldLaunchHistory ->
             if (shouldLaunchHistory) R.string.manage_notifications_history_text
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
index 27b2788..a8a7e88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
@@ -40,6 +40,8 @@
 import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT
 import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED
@@ -48,6 +50,7 @@
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.OldProgress
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.row.shared.ImageModel
 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.MediumSquare
@@ -60,8 +63,9 @@
     fun extractContent(
         entry: NotificationEntry,
         recoveredBuilder: Notification.Builder,
+        @RedactionType redactionType: Int,
         imageModelProvider: ImageModelProvider,
-    ): PromotedNotificationContentModel?
+    ): PromotedNotificationContentModels?
 }
 
 @SysUISingleton
@@ -76,8 +80,9 @@
     override fun extractContent(
         entry: NotificationEntry,
         recoveredBuilder: Notification.Builder,
+        @RedactionType redactionType: Int,
         imageModelProvider: ImageModelProvider,
-    ): PromotedNotificationContentModel? {
+    ): PromotedNotificationContentModels? {
         if (!PromotedNotificationContentModel.featureFlagEnabled()) {
             logger.logExtractionSkipped(entry, "feature flags disabled")
             return null
@@ -95,7 +100,55 @@
             return null
         }
 
-        val contentBuilder = PromotedNotificationContentModel.Builder(entry.key)
+        val privateVersion =
+            extractPrivateContent(
+                key = entry.key,
+                notification = notification,
+                recoveredBuilder = recoveredBuilder,
+                lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs,
+                imageModelProvider = imageModelProvider,
+            )
+        val publicVersion =
+            if (redactionType == REDACTION_TYPE_NONE) {
+                privateVersion
+            } else {
+                if (notification.publicVersion == null) {
+                    privateVersion.toDefaultPublicVersion()
+                } else {
+                    // TODO(b/400991304): implement extraction for [Notification.publicVersion]
+                    privateVersion.toDefaultPublicVersion()
+                }
+            }
+        return PromotedNotificationContentModels(
+                privateVersion = privateVersion,
+                publicVersion = publicVersion,
+            )
+            .also { logger.logExtractionSucceeded(entry, it) }
+    }
+
+    private fun PromotedNotificationContentModel.toDefaultPublicVersion():
+        PromotedNotificationContentModel =
+        PromotedNotificationContentModel.Builder(key = identity.key).let {
+            it.style = if (style == Style.Ineligible) Style.Ineligible else Style.Base
+            it.smallIcon = smallIcon
+            it.iconLevel = iconLevel
+            it.appName = appName
+            it.time = time
+            it.lastAudiblyAlertedMs = lastAudiblyAlertedMs
+            it.profileBadgeResId = profileBadgeResId
+            it.colors = colors
+            it.build()
+        }
+
+    private fun extractPrivateContent(
+        key: String,
+        notification: Notification,
+        recoveredBuilder: Notification.Builder,
+        lastAudiblyAlertedMs: Long,
+        imageModelProvider: ImageModelProvider,
+    ): PromotedNotificationContentModel {
+
+        val contentBuilder = PromotedNotificationContentModel.Builder(key)
 
         // TODO: Pitch a fit if style is unsupported or mandatory fields are missing once
         // FLAG_PROMOTED_ONGOING is set reliably and we're not testing status bar chips.
@@ -108,7 +161,7 @@
         contentBuilder.subText = notification.subText()
         contentBuilder.time = notification.extractWhen()
         contentBuilder.shortCriticalText = notification.shortCriticalText()
-        contentBuilder.lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs
+        contentBuilder.lastAudiblyAlertedMs = lastAudiblyAlertedMs
         contentBuilder.profileBadgeResId = null // TODO
         contentBuilder.title = notification.title(recoveredBuilder.style)
         contentBuilder.text = notification.text(recoveredBuilder.style)
@@ -124,7 +177,7 @@
 
         recoveredBuilder.extractStyleContent(notification, contentBuilder, imageModelProvider)
 
-        return contentBuilder.build().also { logger.logExtractionSucceeded(entry, it) }
+        return contentBuilder.build()
     }
 
     private fun Notification.smallIconModel(imageModelProvider: ImageModelProvider): ImageModel? =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
index 5f9678a..6b6203d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import javax.inject.Inject
 
 @OptIn(ExperimentalStdlibApi::class)
@@ -56,7 +56,7 @@
 
     fun logExtractionSucceeded(
         entry: NotificationEntry,
-        content: PromotedNotificationContentModel,
+        content: PromotedNotificationContentModels,
     ) {
         buffer.log(
             EXTRACTION_TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
index ec4ee45..d9778bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
 @SysUISingleton
@@ -34,6 +35,16 @@
     /** The content to show as the promoted notification on AOD */
     val content: Flow<PromotedNotificationContentModel?> =
         promotedNotificationsInteractor.aodPromotedNotification
+            .map {
+                // TODO(b/400991304): show the private version when unlocked
+                it?.publicVersion
+            }
+            .distinctUntilNewInstance()
 
     val isPresent: Flow<Boolean> = content.map { it != null }.dumpWhileCollecting("isPresent")
+
+    /**
+     * Returns flow where all subsequent repetitions of the same object instance are filtered out.
+     */
+    private fun <T> Flow<T>.distinctUntilNewInstance() = distinctUntilChanged { a, b -> a === b }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
index 96d41f1..08e7528 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
@@ -25,8 +25,8 @@
 import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
 import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style.Ineligible
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import javax.inject.Inject
@@ -201,13 +201,13 @@
      * The top promoted notification represented by a chip, with the order determined by the order
      * of the chips, not the notifications.
      */
-    private val topPromotedChipNotification: Flow<PromotedNotificationContentModel?> =
+    private val topPromotedChipNotification: Flow<PromotedNotificationContentModels?> =
         orderedChipNotifications
             .map { list -> list.firstNotNullOfOrNull { it.promotedContent } }
             .distinctUntilNewInstance()
 
     /** This is the AOD promoted notification, which should avoid regular changing. */
-    val aodPromotedNotification: Flow<PromotedNotificationContentModel?> =
+    val aodPromotedNotification: Flow<PromotedNotificationContentModels?> =
         combine(
                 topPromotedChipNotification,
                 activeNotificationsInteractor.topLevelRepresentativeNotifications,
@@ -229,13 +229,13 @@
             .flowOn(backgroundDispatcher)
 
     private fun List<ActiveNotificationModel>.firstAodEligibleOrNull():
-        PromotedNotificationContentModel? {
+        PromotedNotificationContentModels? {
         return this.firstNotNullOfOrNull { it.promotedContent?.takeIfAodEligible() }
     }
 
-    private fun PromotedNotificationContentModel.takeIfAodEligible():
-        PromotedNotificationContentModel? {
-        return this.takeUnless { it.style == Ineligible }
+    private fun PromotedNotificationContentModels.takeIfAodEligible():
+        PromotedNotificationContentModels? {
+        return this.takeUnless { it.privateVersion.style == Ineligible }
     }
 
     /**
@@ -251,7 +251,7 @@
      */
     private data class NotifAndPromotedContent(
         val key: String,
-        val promotedContent: PromotedNotificationContentModel?,
+        val promotedContent: PromotedNotificationContentModels?,
     ) {
         /**
          * Define the equals of this object to only check the reference equality of the promoted
@@ -269,7 +269,7 @@
         /** Define the hashCode to be very quick, even if it increases collisions. */
         override fun hashCode(): Int {
             var result = key.hashCode()
-            result = 31 * result + (promotedContent?.identity?.hashCode() ?: 0)
+            result = 31 * result + (promotedContent?.key?.hashCode() ?: 0)
             return result
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
index 57b0720..ffacf62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
@@ -29,6 +29,31 @@
 import com.android.systemui.statusbar.notification.row.LazyImage
 import com.android.systemui.statusbar.notification.row.shared.ImageModel
 
+data class PromotedNotificationContentModels(
+    /** The potentially redacted version of the content that will be exposed to the public */
+    val publicVersion: PromotedNotificationContentModel,
+    /** The unredacted version of the content that will be kept private */
+    val privateVersion: PromotedNotificationContentModel,
+) {
+    val key: String
+        get() = privateVersion.identity.key
+
+    init {
+        check(publicVersion.identity.key == privateVersion.identity.key) {
+            "public and private models must have the same key"
+        }
+    }
+
+    fun toRedactedString(): String {
+        val publicVersionString =
+            "==privateVersion".takeIf { privateVersion === publicVersion }
+                ?: publicVersion.toRedactedString()
+        return ("PromotedNotificationContentModels(" +
+            "privateVersion=${privateVersion.toRedactedString()}, " +
+            "publicVersion=$publicVersionString)")
+    }
+}
+
 /**
  * The content needed to render a promoted notification to surfaces besides the notification stack,
  * like the skeleton view on AOD or the status bar chip.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index d97e25f..57ceafc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -60,6 +60,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider;
@@ -1003,7 +1004,7 @@
         row.mImageModelIndex = result.mRowImageInflater.getNewImageIndex();
 
         if (PromotedNotificationContentModel.featureFlagEnabled()) {
-            entry.setPromotedNotificationContentModel(result.mPromotedContent);
+            entry.setPromotedNotificationContentModels(result.mPromotedContent);
         }
 
         boolean setRepliesAndActions = true;
@@ -1387,9 +1388,9 @@
                 mLogger.logAsyncTaskProgress(logKey, "extracting promoted notification content");
                 final ImageModelProvider imageModelProvider =
                         result.mRowImageInflater.useForContentModel();
-                final PromotedNotificationContentModel promotedContent =
+                final PromotedNotificationContentModels promotedContent =
                         mPromotedNotificationContentExtractor.extractContent(mEntry,
-                                recoveredBuilder, imageModelProvider);
+                                recoveredBuilder, mBindParams.redactionType, imageModelProvider);
                 mLogger.logAsyncTaskProgress(logKey, "extracted promoted notification content: "
                         + promotedContent);
 
@@ -1503,7 +1504,7 @@
     static class InflationProgress {
         RowImageInflater mRowImageInflater;
 
-        PromotedNotificationContentModel mPromotedContent;
+        PromotedNotificationContentModels mPromotedContent;
 
         private RemoteViews newContentView;
         private RemoteViews newHeadsUpView;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index ae52db8..4f1b905 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -53,6 +53,7 @@
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
 import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
 import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP
@@ -595,7 +596,7 @@
         val rowImageInflater: RowImageInflater,
         val remoteViews: NewRemoteViews,
         val contentModel: NotificationContentModel,
-        val promotedContent: PromotedNotificationContentModel?,
+        val promotedContent: PromotedNotificationContentModels?,
     ) {
 
         var inflatedContentView: View? = null
@@ -700,7 +701,12 @@
                     )
                     val imageModelProvider = rowImageInflater.useForContentModel()
                     promotedNotificationContentExtractor
-                        .extractContent(entry, builder, imageModelProvider)
+                        .extractContent(
+                            entry,
+                            builder,
+                            bindParams.redactionType,
+                            imageModelProvider,
+                        )
                         .also {
                             logger.logAsyncTaskProgress(
                                 entry.logKey,
@@ -1519,7 +1525,7 @@
 
             entry.setContentModel(result.contentModel)
             if (PromotedNotificationContentModel.featureFlagEnabled()) {
-                entry.promotedNotificationContentModel = result.promotedContent
+                entry.promotedNotificationContentModels = result.promotedContent
             }
 
             result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index 487cbce..9652738 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.stack.PriorityBucket
 
 /**
@@ -88,7 +89,7 @@
      * The content needed to render this as a promoted notification on various surfaces, or null if
      * this notification cannot be rendered as a promoted notification.
      */
-    val promotedContent: PromotedNotificationContentModel?,
+    val promotedContent: PromotedNotificationContentModels?,
 ) : ActiveNotificationEntryModel() {
     init {
         if (!PromotedNotificationContentModel.featureFlagEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 61b7d80..b7eada1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -38,7 +38,7 @@
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
@@ -347,7 +347,7 @@
          * If the call notification also meets promoted notification criteria, this field is filled
          * in with the content related to promotion. Otherwise null.
          */
-        val promotedContent: PromotedNotificationContentModel?,
+        val promotedContent: PromotedNotificationContentModels?,
         /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
         val isOngoing: Boolean,
         /** True if the user has swiped away the status bar while in this phone call. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
index 322dfff..9546d37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
@@ -18,7 +18,7 @@
 
 import android.app.PendingIntent
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 
 /** Represents the state of any ongoing calls. */
 sealed interface OngoingCallModel {
@@ -47,7 +47,7 @@
         val intent: PendingIntent?,
         val notificationKey: String,
         val appName: String,
-        val promotedContent: PromotedNotificationContentModel?,
+        val promotedContent: PromotedNotificationContentModels?,
         val isAppVisible: Boolean,
     ) : OngoingCallModel
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 206654a..9b314f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -1500,6 +1500,7 @@
                 mKosmos.getKeyguardInteractor(),
                 mKeyguardTransitionBootInteractor,
                 mKosmos::getCommunalSceneInteractor,
+                mKosmos::getCommunalSettingsInteractor,
                 mock(WindowManagerOcclusionManager.class));
         mViewMediator.mUserChangedCallback = mUserTrackerCallback;
         mViewMediator.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
index 86f7966..d6b778f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
@@ -35,8 +35,14 @@
 import com.android.systemui.animation.activityTransitionAnimator
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
 import com.android.systemui.communal.data.repository.communalSceneRepository
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel
 import com.android.systemui.concurrency.fakeExecutor
@@ -81,8 +87,11 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
 
 /** Kotlin version of KeyguardViewMediatorTest to allow for coroutine testing. */
 @SmallTest
@@ -152,6 +161,7 @@
                 keyguardInteractor,
                 keyguardTransitionBootInteractor,
                 { communalSceneInteractor },
+                { communalSettingsInteractor },
                 mock<WindowManagerOcclusionManager>(),
             )
         }
@@ -164,6 +174,10 @@
     @Test
     fun doKeyguardTimeout_changesCommunalScene() =
         kosmos.runTest {
+            // Hub is enabled and hub condition is active.
+            setCommunalV2Enabled(true)
+            enableHubOnCharging()
+
             // doKeyguardTimeout message received.
             val timeoutOptions = Bundle()
             timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
@@ -174,4 +188,56 @@
             assertThat(communalSceneRepository.currentScene.value)
                 .isEqualTo(CommunalScenes.Communal)
         }
+
+    @Test
+    fun doKeyguardTimeout_communalNotAvailable_sleeps() =
+        kosmos.runTest {
+            // Hub disabled.
+            setCommunalV2Enabled(false)
+
+            // doKeyguardTimeout message received.
+            val timeoutOptions = Bundle()
+            timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
+            underTest.doKeyguardTimeout(timeoutOptions)
+            testableLooper.processAllMessages()
+
+            // Sleep is requested.
+            verify(powerManager)
+                .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0))
+
+            // Hub scene is not changed.
+            assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank)
+        }
+
+    @Test
+    fun doKeyguardTimeout_hubConditionNotActive_sleeps() =
+        kosmos.runTest {
+            // Communal enabled, but hub condition set to never.
+            setCommunalV2Enabled(true)
+            disableHubShowingAutomatically()
+
+            // doKeyguardTimeout message received.
+            val timeoutOptions = Bundle()
+            timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
+            underTest.doKeyguardTimeout(timeoutOptions)
+            testableLooper.processAllMessages()
+
+            // Sleep is requested.
+            verify(powerManager)
+                .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0))
+
+            // Hub scene is not changed.
+            assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank)
+        }
+
+    private fun Kosmos.enableHubOnCharging() {
+        communalSettingsInteractor.setSuppressionReasons(emptyList())
+        batteryRepository.fake.setDevicePluggedIn(true)
+    }
+
+    private fun Kosmos.disableHubShowingAutomatically() {
+        communalSettingsInteractor.setSuppressionReasons(
+            listOf(SuppressionReason.ReasonUnknown(FEATURE_AUTO_OPEN))
+        )
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
index 997cf41..f4d0c26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
@@ -18,9 +18,11 @@
 
 import android.hardware.devicestate.feature.flags.Flags.FLAG_DEVICE_STATE_RDM_V2
 import android.hardware.display.rearDisplay
+import android.os.fakeExecutorHandler
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.view.Display
+import android.view.accessibility.accessibilityManager
 import androidx.test.filters.SmallTest
 import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.SysuiTestCase
@@ -62,6 +64,8 @@
             kosmos.rearDisplayInnerDialogDelegateFactory,
             kosmos.testScope,
             kosmos.keyguardUpdateMonitor,
+            kosmos.accessibilityManager,
+            kosmos.fakeExecutorHandler,
         )
 
     @Before
@@ -69,7 +73,7 @@
         whenever(kosmos.rearDisplay.flags).thenReturn(Display.FLAG_REAR)
         whenever(kosmos.rearDisplay.displayAdjustments)
             .thenReturn(mContext.display.displayAdjustments)
-        whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any()))
+        whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any(), any()))
             .thenReturn(mockDelegate)
         whenever(mockDelegate.createDialog()).thenReturn(mockDialog)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
index fc76616..477e5ba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
@@ -17,7 +17,10 @@
 package com.android.systemui.reardisplay
 
 import android.testing.TestableLooper
+import android.view.View
+import android.widget.Button
 import android.widget.SeekBar
+import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.haptics.msdl.msdlPlayer
@@ -28,6 +31,7 @@
 import com.android.systemui.statusbar.phone.systemUIDialogDotFactory
 import com.android.systemui.testKosmos
 import com.android.systemui.util.time.systemClock
+import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import org.junit.Test
@@ -49,6 +53,7 @@
             RearDisplayInnerDialogDelegate(
                 kosmos.systemUIDialogDotFactory,
                 mContext,
+                false /* touchExplorationEnabled */,
                 kosmos.vibratorHelper,
                 kosmos.msdlPlayer,
                 kosmos.systemClock,
@@ -68,6 +73,7 @@
         RearDisplayInnerDialogDelegate(
                 kosmos.systemUIDialogDotFactory,
                 mContext,
+                false /* touchExplorationEnabled */,
                 kosmos.vibratorHelper,
                 kosmos.msdlPlayer,
                 kosmos.systemClock,
@@ -78,6 +84,9 @@
             .apply {
                 show()
                 val seekbar = findViewById<SeekBar>(R.id.seekbar)
+                assertThat(seekbar.visibility).isEqualTo(View.VISIBLE)
+                assertThat(findViewById<TextView>(R.id.seekbar_instructions).visibility)
+                    .isEqualTo(View.VISIBLE)
                 seekbar.progress = 50
                 seekbar.progress = 100
                 verify(mockCallback).run()
@@ -90,6 +99,7 @@
         RearDisplayInnerDialogDelegate(
                 kosmos.systemUIDialogDotFactory,
                 mContext,
+                false /* touchExplorationEnabled */,
                 kosmos.vibratorHelper,
                 kosmos.msdlPlayer,
                 kosmos.systemClock,
@@ -118,4 +128,33 @@
         // Progress is reset
         verify(mockSeekbar).setProgress(eq(0))
     }
+
+    @Test
+    fun testTouchExplorationEnabled() {
+        val mockCallback = mock<Runnable>()
+
+        RearDisplayInnerDialogDelegate(
+                kosmos.systemUIDialogDotFactory,
+                mContext,
+                true /* touchExplorationEnabled */,
+                kosmos.vibratorHelper,
+                kosmos.msdlPlayer,
+                kosmos.systemClock,
+            ) {
+                mockCallback.run()
+            }
+            .createDialog()
+            .apply {
+                show()
+                assertThat(findViewById<SeekBar>(R.id.seekbar).visibility).isEqualTo(View.GONE)
+                assertThat(findViewById<TextView>(R.id.seekbar_instructions).visibility)
+                    .isEqualTo(View.GONE)
+
+                val cancelButton = findViewById<Button>(R.id.cancel_button)
+                assertThat(cancelButton.visibility).isEqualTo(View.VISIBLE)
+
+                cancelButton.performClick()
+                verify(mockCallback).run()
+            }
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index 846db63..2facc1c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -283,6 +283,9 @@
     }
 
     public FakeBroadcastDispatcher getFakeBroadcastDispatcher() {
+        if (mSysuiDependency == null) {
+            return null;
+        }
         return mSysuiDependency.getFakeBroadcastDispatcher();
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
index 165f85d..4af4e80 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -19,7 +19,7 @@
 import android.app.PendingIntent
 import android.graphics.drawable.Icon
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN
@@ -49,7 +49,7 @@
     contentIntent: PendingIntent? = null,
     bucket: Int = BUCKET_UNKNOWN,
     callType: CallType = CallType.None,
-    promotedContent: PromotedNotificationContentModel? = null,
+    promotedContent: PromotedNotificationContentModels? = null,
 ) =
     ActiveNotificationModel(
         key = key,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
index 99323db..ebe20af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor
 
 val Kosmos.footerViewModel by Fixture {
     FooterViewModel(
@@ -29,6 +30,7 @@
         notificationSettingsInteractor = notificationSettingsInteractor,
         seenNotificationsInteractor = seenNotificationsInteractor,
         shadeInteractor = shadeInteractor,
+        windowRootViewBlurInteractor = windowRootViewBlurInteractor,
     )
 }
 val Kosmos.footerViewModelFactory: FooterViewModel.Factory by Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
index 8fdf5db..aaa86aa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
@@ -17,21 +17,23 @@
 package com.android.systemui.statusbar.notification.promoted
 
 import android.app.Notification
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
 import org.junit.Assert
 
 class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtractor {
     @JvmField
-    val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModel?>()
+    val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModels?>()
     @JvmField val extractCalls = mutableListOf<Pair<NotificationEntry, Notification.Builder>>()
 
     override fun extractContent(
         entry: NotificationEntry,
         recoveredBuilder: Notification.Builder,
+        @RedactionType redactionType: Int,
         imageModelProvider: ImageModelProvider,
-    ): PromotedNotificationContentModel? {
+    ): PromotedNotificationContentModels? {
         extractCalls.add(entry to recoveredBuilder)
 
         if (contentForEntry.isEmpty()) {
@@ -44,7 +46,7 @@
         }
     }
 
-    fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModel?) {
+    fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModels?) {
         contentForEntry.clear()
         contentForEntry[entry] = content
         extractCalls.clear()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
index 2b3158d..c4542c4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
@@ -19,6 +19,7 @@
 import android.app.Notification
 import android.content.applicationContext
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.row.RowImageInflater
 import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform
@@ -39,9 +40,10 @@
         promotedNotificationContentExtractor.extractContent(
             entry,
             Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification),
+            REDACTION_TYPE_NONE,
             RowImageInflater.newInstance(previousIndex = null, reinflating = false)
                 .useForContentModel(),
         )
-    entry.promotedNotificationContentModel =
+    entry.promotedNotificationContentModels =
         requireNotNull(extractedContent) { "extractContent returned null" }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt
new file mode 100644
index 0000000..6916d560
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 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.systemui.statusbar.notification.promoted.shared.model
+
+class PromotedNotificationContentBuilder(val key: String) {
+    private val sharedBuilder = PromotedNotificationContentModel.Builder(key)
+
+    fun applyToShared(
+        block: PromotedNotificationContentModel.Builder.() -> Unit
+    ): PromotedNotificationContentBuilder {
+        sharedBuilder.apply(block)
+        return this
+    }
+
+    fun build(): PromotedNotificationContentModels {
+        val sharedModel = sharedBuilder.build()
+        return PromotedNotificationContentModels(sharedModel, sharedModel)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
index 3e96fd7..e5e1a83 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -26,7 +26,7 @@
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.addNotif
 import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -39,7 +39,7 @@
     intent: PendingIntent? = null,
     notificationKey: String = "test",
     appName: String = "",
-    promotedContent: PromotedNotificationContentModel? = null,
+    promotedContent: PromotedNotificationContentModels? = null,
     isAppVisible: Boolean = false,
 ) =
     OngoingCallModel.InCall(
@@ -77,7 +77,7 @@
         key: String = "notif",
         startTimeMs: Long = 1000L,
         statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(),
-        promotedContent: PromotedNotificationContentModel? = null,
+        promotedContent: PromotedNotificationContentModels? = null,
         contentIntent: PendingIntent? = null,
         uid: Int = DEFAULT_UID,
         appName: String = "Fake name",
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 0b9c45d..60343e9 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -152,9 +152,20 @@
                     if (direction == AutoclickScrollPanel.DIRECTION_EXIT) {
                         return;
                     }
-                    // For direction buttons, perform scroll action immediately.
-                    if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) {
-                        handleScroll(direction);
+
+                    // Handle all non-exit buttons when hovered.
+                    if (hovered) {
+                        // Clear the indicator.
+                        if (mAutoclickIndicatorScheduler != null) {
+                            mAutoclickIndicatorScheduler.cancel();
+                            if (mAutoclickIndicatorView != null) {
+                                mAutoclickIndicatorView.clearIndicator();
+                            }
+                        }
+                        // Perform scroll action.
+                        if (direction != DIRECTION_NONE) {
+                            handleScroll(direction);
+                        }
                     }
                 }
             };
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 6b3661a..a8bb523 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -186,6 +186,7 @@
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
 import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
 import android.media.permission.ClearCallingIdentityContext;
 import android.media.permission.SafeCloseable;
 import android.media.projection.IMediaProjection;
@@ -1388,6 +1389,7 @@
         mUseVolumeGroupAliases = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups);
 
+        mAudioVolumeChangeHandler = new AudioVolumeChangeHandler(mAudioSystem);
         // Initialize volume
         // Priority 1 - Android Property
         // Priority 2 - Audio Policy Service
@@ -4452,6 +4454,27 @@
         }
     }
 
+    //================================
+    // Audio Volume Change Dispatcher
+    //================================
+    private final AudioVolumeChangeHandler mAudioVolumeChangeHandler;
+
+    /** @see AudioManager#registerVolumeGroupCallback(executor, callback) */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) {
+        super.registerAudioVolumeCallback_enforcePermission();
+        mAudioVolumeChangeHandler.registerListener(callback);
+    }
+
+    /** @see AudioManager#unregisterVolumeGroupCallback(callback) */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) {
+        super.unregisterAudioVolumeCallback_enforcePermission();
+        mAudioVolumeChangeHandler.unregisterListener(callback);
+    }
+
     @Override
     @android.annotation.EnforcePermission(anyOf = {
             MODIFY_AUDIO_SETTINGS_PRIVILEGED, MODIFY_AUDIO_ROUTING })
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index a6267c1..ced5fae 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -23,6 +23,7 @@
 import android.media.AudioMixerAttributes;
 import android.media.AudioSystem;
 import android.media.IDevicesForAttributesCallback;
+import android.media.INativeAudioVolumeGroupCallback;
 import android.media.ISoundDose;
 import android.media.ISoundDoseCallback;
 import android.media.audiopolicy.AudioMix;
@@ -758,6 +759,29 @@
     }
 
     /**
+     * Same as {@link AudioSystem#registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)}
+     * @param callback to register
+     * @return {@link #SUCCESS} if successfully registered.
+     *
+     * @hide
+     */
+    public int registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) {
+        return AudioSystem.registerAudioVolumeGroupCallback(callback);
+    }
+
+    /**
+     * Same as
+     * {@link AudioSystem#unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)}.
+     * @param callback to register
+     * @return {@link #SUCCESS} if successfully registered.
+     *
+     * @hide
+     */
+    public int unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) {
+        return AudioSystem.unregisterAudioVolumeGroupCallback(callback);
+    }
+
+    /**
      * Part of AudioService dump
      * @param pw
      */
diff --git a/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java
new file mode 100644
index 0000000..2bb4301
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 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.server.audio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.INativeAudioVolumeGroupCallback;
+import android.media.audio.common.AudioVolumeGroupChangeEvent;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+/**
+ * The AudioVolumeChangeHandler handles AudioVolume callbacks invoked by native
+ * {@link INativeAudioVolumeGroupCallback} callback.
+ */
+/* private package */ class AudioVolumeChangeHandler {
+    private static final String TAG = "AudioVolumeChangeHandler";
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final RemoteCallbackList<IAudioVolumeChangeDispatcher> mListeners =
+            new RemoteCallbackList<>();
+    private final @NonNull AudioSystemAdapter mAudioSystem;
+    private @Nullable AudioVolumeGroupCallback mAudioVolumeGroupCallback;
+
+    AudioVolumeChangeHandler(@NonNull AudioSystemAdapter asa) {
+        mAudioSystem = asa;
+    }
+
+    @GuardedBy("mLock")
+    private void lazyInitLocked() {
+        mAudioVolumeGroupCallback = new AudioVolumeGroupCallback();
+        mAudioSystem.registerAudioVolumeGroupCallback(mAudioVolumeGroupCallback);
+    }
+
+    private void sendAudioVolumeGroupChangedToClients(int groupId, int index) {
+        RemoteCallbackList<IAudioVolumeChangeDispatcher> listeners;
+        int nbDispatchers;
+        synchronized (mLock) {
+            listeners = mListeners;
+            nbDispatchers = mListeners.beginBroadcast();
+        }
+        for (int i = 0; i < nbDispatchers; i++) {
+            try {
+                listeners.getBroadcastItem(i).onAudioVolumeGroupChanged(groupId, index);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to broadcast Volume Changed event");
+            }
+        }
+        synchronized (mLock) {
+            mListeners.finishBroadcast();
+        }
+    }
+
+   /**
+    * @param cb the {@link IAudioVolumeChangeDispatcher} to register
+    */
+    public void registerListener(@NonNull IAudioVolumeChangeDispatcher cb) {
+        Preconditions.checkNotNull(cb, "Volume group callback must not be null");
+        synchronized (mLock) {
+            if (mAudioVolumeGroupCallback == null) {
+                lazyInitLocked();
+            }
+            mListeners.register(cb);
+        }
+    }
+
+   /**
+    * @param cb the {@link IAudioVolumeChangeDispatcher} to unregister
+    */
+    public void unregisterListener(@NonNull IAudioVolumeChangeDispatcher cb) {
+        Preconditions.checkNotNull(cb, "Volume group callback must not be null");
+        synchronized (mLock) {
+            mListeners.unregister(cb);
+        }
+    }
+
+    private final class AudioVolumeGroupCallback extends INativeAudioVolumeGroupCallback.Stub {
+        public void onAudioVolumeGroupChanged(AudioVolumeGroupChangeEvent volumeEvent) {
+            Slog.v(TAG, "onAudioVolumeGroupChanged volumeEvent=" + volumeEvent);
+            sendAudioVolumeGroupChangedToClients(volumeEvent.groupId, volumeEvent.flags);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 41b0b4d..a2d0654 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -252,17 +252,22 @@
     static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_EARC =
             new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
                     AudioDeviceInfo.TYPE_HDMI_EARC, "");
+    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_LINE_DIGITAL =
+            new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+            AudioDeviceInfo.TYPE_LINE_DIGITAL, "");
 
     // Audio output devices used for absolute volume behavior
     private static final List<AudioDeviceAttributes> AVB_AUDIO_OUTPUT_DEVICES =
             List.of(AUDIO_OUTPUT_DEVICE_HDMI,
                     AUDIO_OUTPUT_DEVICE_HDMI_ARC,
-                    AUDIO_OUTPUT_DEVICE_HDMI_EARC);
+                    AUDIO_OUTPUT_DEVICE_HDMI_EARC,
+                    AUDIO_OUTPUT_DEVICE_LINE_DIGITAL);
 
     // Audio output devices used for absolute volume behavior on TV panels
     private static final List<AudioDeviceAttributes> TV_AVB_AUDIO_OUTPUT_DEVICES =
             List.of(AUDIO_OUTPUT_DEVICE_HDMI_ARC,
-                    AUDIO_OUTPUT_DEVICE_HDMI_EARC);
+                    AUDIO_OUTPUT_DEVICE_HDMI_EARC,
+                    AUDIO_OUTPUT_DEVICE_LINE_DIGITAL);
 
     // Audio output devices used for absolute volume behavior on Playback devices
     private static final List<AudioDeviceAttributes> PLAYBACK_AVB_AUDIO_OUTPUT_DEVICES =
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index ccb9e3e..bbf7732 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -33,6 +33,7 @@
 import android.hardware.contexthub.Reason;
 import android.hardware.location.ContextHubTransaction;
 import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoAppState;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.PowerManager;
@@ -48,6 +49,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -182,8 +184,11 @@
                 long expiryMillis = RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT.toMillis();
                 if (nowMillis >= nextEntry.getValue() + expiryMillis) {
                     iterator.remove();
+                } else {
+                    // Safe to break since LinkedHashMap is insertion-ordered, so the next entry
+                    // will have a later timestamp and will not be expired.
+                    break;
                 }
-                break;
             }
 
             return mRxMessageHistoryMap.containsKey(message.getMessageSequenceNumber());
@@ -276,6 +281,7 @@
 
         int sessionId = mEndpointManager.reserveSessionId();
         EndpointInfo halEndpointInfo = ContextHubServiceUtil.convertHalEndpointInfo(destination);
+        Log.d(TAG, "openSession: sessionId=" + sessionId);
 
         synchronized (mOpenSessionLock) {
             try {
@@ -301,6 +307,7 @@
             throw new IllegalArgumentException(
                     "Unknown session ID in closeSession: id=" + sessionId);
         }
+        Log.d(TAG, "closeSession: sessionId=" + sessionId + " reason=" + reason);
         mEndpointManager.halCloseEndpointSession(
                 sessionId, ContextHubServiceUtil.toHalReason(reason));
     }
@@ -373,12 +380,43 @@
                 try {
                     mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
                 } catch (RemoteException e) {
-                    Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+                    Log.e(
+                            TAG,
+                            "Exception while sending message on session "
+                                    + sessionId
+                                    + ", closing session",
+                            e);
+                    notifySessionClosedToBoth(sessionId, Reason.UNSPECIFIED);
                 }
             } else {
+                IContextHubTransactionCallback wrappedCallback =
+                        new IContextHubTransactionCallback.Stub() {
+                            @Override
+                            public void onQueryResponse(int result, List<NanoAppState> appStates)
+                                    throws RemoteException {
+                                Log.w(TAG, "Unexpected onQueryResponse callback");
+                            }
+
+                            @Override
+                            public void onTransactionComplete(int result) throws RemoteException {
+                                callback.onTransactionComplete(result);
+                                if (result != ContextHubTransaction.RESULT_SUCCESS) {
+                                    Log.e(
+                                            TAG,
+                                            "Failed to send reliable message "
+                                                    + message
+                                                    + ", closing session");
+                                    notifySessionClosedToBoth(sessionId, Reason.UNSPECIFIED);
+                                }
+                            }
+                        };
                 ContextHubServiceTransaction transaction =
                         mTransactionManager.createSessionMessageTransaction(
-                                mHubInterface, sessionId, halMessage, mPackageName, callback);
+                                mHubInterface,
+                                sessionId,
+                                halMessage,
+                                mPackageName,
+                                wrappedCallback);
                 try {
                     mTransactionManager.addTransaction(transaction);
                     info.setReliableMessagePending(transaction.getMessageSequenceNumber());
@@ -445,10 +483,7 @@
                     int id = mSessionMap.keyAt(i);
                     HubEndpointInfo target = mSessionMap.get(id).getRemoteEndpointInfo();
                     if (!hasEndpointPermissions(target)) {
-                        mEndpointManager.halCloseEndpointSessionNoThrow(
-                                id, Reason.PERMISSION_DENIED);
-                        onCloseEndpointSession(id, Reason.PERMISSION_DENIED);
-                        // Resource cleanup is done in onCloseEndpointSession
+                        notifySessionClosedToBoth(id, Reason.PERMISSION_DENIED);
                     }
                 }
             }
@@ -532,8 +567,17 @@
 
     /* package */ void onMessageReceived(int sessionId, HubMessage message) {
         byte errorCode = onMessageReceivedInternal(sessionId, message);
-        if (errorCode != ErrorCode.OK && message.isResponseRequired()) {
-            sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode);
+        if (errorCode != ErrorCode.OK) {
+            Log.e(TAG, "Failed to send message to endpoint: " + message + ", closing session");
+            if (message.isResponseRequired()) {
+                sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode);
+            } else {
+                notifySessionClosedToBoth(
+                        sessionId,
+                        (errorCode == ErrorCode.PERMISSION_DENIED)
+                                ? Reason.PERMISSION_DENIED
+                                : Reason.UNSPECIFIED);
+            }
         }
     }
 
@@ -800,4 +844,16 @@
                         + "-0x"
                         + Long.toHexString(endpoint.getIdentifier().getEndpoint()));
     }
+
+    /**
+     * Notifies to both the HAL and the app that a session has been closed.
+     *
+     * @param sessionId The ID of the session that was closed
+     * @param halReason The HAL reason for closing the session
+     */
+    private void notifySessionClosedToBoth(int sessionId, byte halReason) {
+        Log.d(TAG, "notifySessionClosedToBoth: sessionId=" + sessionId + ", reason=" + halReason);
+        mEndpointManager.halCloseEndpointSessionNoThrow(sessionId, halReason);
+        onCloseEndpointSession(sessionId, halReason);
+    }
 }
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
index cf8b703..05aac55 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
@@ -18,13 +18,20 @@
 
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.hardware.tv.mediaquality.ColorRange;
+import android.hardware.tv.mediaquality.ColorSpace;
+import android.hardware.tv.mediaquality.ColorTemperature;
 import android.hardware.tv.mediaquality.DolbyAudioProcessing;
 import android.hardware.tv.mediaquality.DtsVirtualX;
+import android.hardware.tv.mediaquality.Gamma;
 import android.hardware.tv.mediaquality.ParameterDefaultValue;
 import android.hardware.tv.mediaquality.ParameterName;
 import android.hardware.tv.mediaquality.ParameterRange;
 import android.hardware.tv.mediaquality.PictureParameter;
+import android.hardware.tv.mediaquality.PictureQualityEventType;
+import android.hardware.tv.mediaquality.QualityLevel;
 import android.hardware.tv.mediaquality.SoundParameter;
+import android.media.quality.MediaQualityContract;
 import android.media.quality.MediaQualityContract.BaseParameters;
 import android.media.quality.MediaQualityContract.PictureQuality;
 import android.media.quality.MediaQualityContract.SoundQuality;
@@ -371,7 +378,7 @@
         }
         List<PictureParameter> pictureParams = new ArrayList<>();
         if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) {
-            pictureParams.add(PictureParameter.brightness(params.getLong(
+            pictureParams.add(PictureParameter.brightness((float) params.getDouble(
                     PictureQuality.PARAMETER_BRIGHTNESS)));
             params.remove(PictureQuality.PARAMETER_BRIGHTNESS);
         }
@@ -441,28 +448,46 @@
             params.remove(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN);
         }
         if (params.containsKey(PictureQuality.PARAMETER_NOISE_REDUCTION)) {
-            pictureParams.add(PictureParameter.noiseReduction(
-                    (byte) params.getInt(PictureQuality.PARAMETER_NOISE_REDUCTION)));
+            String noiseReductionString = params.getString(
+                    PictureQuality.PARAMETER_NOISE_REDUCTION);
+            if (noiseReductionString != null) {
+                byte noiseReductionByte = mapQualityLevel(noiseReductionString);
+                pictureParams.add(PictureParameter.noiseReduction(noiseReductionByte));
+            }
             params.remove(PictureQuality.PARAMETER_NOISE_REDUCTION);
         }
         if (params.containsKey(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)) {
-            pictureParams.add(PictureParameter.mpegNoiseReduction(
-                    (byte) params.getInt(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)));
+            String mpegNoiseReductionString = params.getString(
+                    PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION);
+            if (mpegNoiseReductionString != null) {
+                byte mpegNoiseReductionByte = mapQualityLevel(mpegNoiseReductionString);
+                pictureParams.add(PictureParameter.mpegNoiseReduction(mpegNoiseReductionByte));
+            }
             params.remove(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION);
         }
         if (params.containsKey(PictureQuality.PARAMETER_FLESH_TONE)) {
-            pictureParams.add(PictureParameter.fleshTone(
-                    (byte) params.getInt(PictureQuality.PARAMETER_FLESH_TONE)));
+            String fleshToneString = params.getString(PictureQuality.PARAMETER_FLESH_TONE);
+            if (fleshToneString != null) {
+                byte fleshToneByte = mapQualityLevel(fleshToneString);
+                pictureParams.add(PictureParameter.fleshTone(fleshToneByte));
+            }
             params.remove(PictureQuality.PARAMETER_FLESH_TONE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_DECONTOUR)) {
-            pictureParams.add(PictureParameter.deContour(
-                    (byte) params.getInt(PictureQuality.PARAMETER_DECONTOUR)));
+            String decontourString = params.getString(PictureQuality.PARAMETER_DECONTOUR);
+            if (decontourString != null) {
+                byte decontourByte = mapQualityLevel(decontourString);
+                pictureParams.add(PictureParameter.deContour(decontourByte));
+            }
             params.remove(PictureQuality.PARAMETER_DECONTOUR);
         }
         if (params.containsKey(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)) {
-            pictureParams.add(PictureParameter.dynamicLumaControl(
-                    (byte) params.getInt(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)));
+            String dynamicLunaControlString = params.getString(
+                    PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL);
+            if (dynamicLunaControlString != null) {
+                byte dynamicLunaControlByte = mapQualityLevel(dynamicLunaControlString);
+                pictureParams.add(PictureParameter.dynamicLumaControl(dynamicLunaControlByte));
+            }
             params.remove(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL);
         }
         if (params.containsKey(PictureQuality.PARAMETER_FILM_MODE)) {
@@ -481,9 +506,48 @@
             params.remove(PictureQuality.PARAMETER_COLOR_TUNE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE)) {
-            pictureParams.add(PictureParameter.colorTemperature(
-                    (byte) params.getInt(
-                            PictureQuality.PARAMETER_COLOR_TEMPERATURE)));
+            String colorTemperatureString = params.getString(
+                    PictureQuality.PARAMETER_COLOR_TEMPERATURE);
+            if (colorTemperatureString != null) {
+                byte colorTemperatureByte;
+                switch (colorTemperatureString) {
+                    case MediaQualityContract.COLOR_TEMP_USER:
+                        colorTemperatureByte = ColorTemperature.USER;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_COOL:
+                        colorTemperatureByte = ColorTemperature.COOL;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_STANDARD:
+                        colorTemperatureByte = ColorTemperature.STANDARD;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_WARM:
+                        colorTemperatureByte = ColorTemperature.WARM;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_USER_HDR10PLUS:
+                        colorTemperatureByte = ColorTemperature.USER_HDR10PLUS;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_COOL_HDR10PLUS:
+                        colorTemperatureByte = ColorTemperature.COOL_HDR10PLUS;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_STANDARD_HDR10PLUS:
+                        colorTemperatureByte = ColorTemperature.STANDARD_HDR10PLUS;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_WARM_HDR10PLUS:
+                        colorTemperatureByte = ColorTemperature.WARM_HDR10PLUS;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_FMMSDR:
+                        colorTemperatureByte = ColorTemperature.FMMSDR;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_FMMHDR:
+                        colorTemperatureByte = ColorTemperature.FMMHDR;
+                        break;
+                    default:
+                        colorTemperatureByte = ColorTemperature.STANDARD;
+                        Log.e("PictureParams", "Invalid color_temp string: "
+                                + colorTemperatureString);
+                }
+                pictureParams.add(PictureParameter.colorTemperature(colorTemperatureByte));
+            }
             params.remove(PictureQuality.PARAMETER_COLOR_TEMPERATURE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_GLOBAL_DIMMING)) {
@@ -517,8 +581,26 @@
             params.remove(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN);
         }
         if (params.containsKey(PictureQuality.PARAMETER_LEVEL_RANGE)) {
-            pictureParams.add(PictureParameter.levelRange(
-                    (byte) params.getInt(PictureQuality.PARAMETER_LEVEL_RANGE)));
+            String levelRangeString = params.getString(PictureQuality.PARAMETER_LEVEL_RANGE);
+            if (levelRangeString != null) {
+                byte levelRangeByte;
+                switch (levelRangeString) {
+                    case "AUTO":
+                        levelRangeByte = ColorRange.AUTO;
+                        break;
+                    case "LIMITED":
+                        levelRangeByte = ColorRange.LIMITED;
+                        break;
+                    case "FULL":
+                        levelRangeByte = ColorRange.FULL;
+                        break;
+                    default:
+                        levelRangeByte = ColorRange.AUTO;
+                        Log.e("PictureParams", "Invalid color_range string: "
+                                + levelRangeString);
+                }
+                pictureParams.add(PictureParameter.levelRange(levelRangeByte));
+            }
             params.remove(PictureQuality.PARAMETER_LEVEL_RANGE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_GAMUT_MAPPING)) {
@@ -547,13 +629,61 @@
             params.remove(PictureQuality.PARAMETER_CVRR);
         }
         if (params.containsKey(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) {
-            pictureParams.add(PictureParameter.hdmiRgbRange(
-                    (byte) params.getInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE)));
+            String hdmiRgbRangeString = params.getString(PictureQuality.PARAMETER_HDMI_RGB_RANGE);
+            if (hdmiRgbRangeString != null) {
+                byte hdmiRgbRangeByte;
+                switch (hdmiRgbRangeString) {
+                    case "AUTO":
+                        hdmiRgbRangeByte = ColorRange.AUTO;
+                        break;
+                    case "LIMITED":
+                        hdmiRgbRangeByte = ColorRange.LIMITED;
+                        break;
+                    case "FULL":
+                        hdmiRgbRangeByte = ColorRange.FULL;
+                        break;
+                    default:
+                        hdmiRgbRangeByte = ColorRange.AUTO;
+                        Log.e("PictureParams", "Invalid hdmi_rgb_range string: "
+                                + hdmiRgbRangeByte);
+                }
+                pictureParams.add(PictureParameter.hdmiRgbRange(hdmiRgbRangeByte));
+            }
             params.remove(PictureQuality.PARAMETER_HDMI_RGB_RANGE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_COLOR_SPACE)) {
-            pictureParams.add(PictureParameter.colorSpace(
-                    (byte) params.getInt(PictureQuality.PARAMETER_COLOR_SPACE)));
+            String colorSpaceString = params.getString(PictureQuality.PARAMETER_COLOR_SPACE);
+            if (colorSpaceString != null) {
+                byte colorSpaceByte;
+                switch (colorSpaceString) {
+                    case "AUTO":
+                        colorSpaceByte = ColorSpace.AUTO;
+                        break;
+                    case "S_RGB_BT_709":
+                        colorSpaceByte = ColorSpace.S_RGB_BT_709;
+                        break;
+                    case "DCI":
+                        colorSpaceByte = ColorSpace.DCI;
+                        break;
+                    case "ADOBE_RGB":
+                        colorSpaceByte = ColorSpace.ADOBE_RGB;
+                        break;
+                    case "BT2020":
+                        colorSpaceByte = ColorSpace.BT2020;
+                        break;
+                    case "ON":
+                        colorSpaceByte = ColorSpace.ON;
+                        break;
+                    case "OFF":
+                        colorSpaceByte = ColorSpace.OFF;
+                        break;
+                    default:
+                        colorSpaceByte = ColorSpace.OFF;
+                        Log.e("PictureParams", "Invalid color_space string: "
+                                + colorSpaceString);
+                }
+                pictureParams.add(PictureParameter.colorSpace(colorSpaceByte));
+            }
             params.remove(PictureQuality.PARAMETER_COLOR_SPACE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)) {
@@ -567,8 +697,25 @@
             params.remove(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID);
         }
         if (params.containsKey(PictureQuality.PARAMETER_GAMMA)) {
-            pictureParams.add(PictureParameter.gamma(
-                    (byte) params.getInt(PictureQuality.PARAMETER_GAMMA)));
+            String gammaString = params.getString(PictureQuality.PARAMETER_GAMMA);
+            if (gammaString != null) {
+                byte gammaByte;
+                switch (gammaString) {
+                    case "DARK":
+                        gammaByte = Gamma.DARK;
+                        break;
+                    case "MIDDLE":
+                        gammaByte = Gamma.MIDDLE;
+                        break;
+                    case "BRIGHT":
+                        gammaByte = Gamma.BRIGHT;
+                        break;
+                    default:
+                        gammaByte = Gamma.DARK;
+                        Log.e("PictureParams", "Invalid gamma string: " + gammaString);
+                }
+                pictureParams.add(PictureParameter.gamma(gammaByte));
+            }
             params.remove(PictureQuality.PARAMETER_GAMMA);
         }
         if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) {
@@ -602,13 +749,19 @@
             params.remove(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) {
-            pictureParams.add(PictureParameter.lowBlueLight(
-                    (byte) params.getInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)));
+            String lowBlueLightString = params.getString(PictureQuality.PARAMETER_LOW_BLUE_LIGHT);
+            if (lowBlueLightString != null) {
+                byte lowBlueLightByte = mapQualityLevel(lowBlueLightString);
+                pictureParams.add(PictureParameter.lowBlueLight(lowBlueLightByte));
+            }
             params.remove(PictureQuality.PARAMETER_LOW_BLUE_LIGHT);
         }
         if (params.containsKey(PictureQuality.PARAMETER_LD_MODE)) {
-            pictureParams.add(PictureParameter.LdMode(
-                    (byte) params.getInt(PictureQuality.PARAMETER_LD_MODE)));
+            String ldModeString = params.getString(PictureQuality.PARAMETER_LD_MODE);
+            if (ldModeString != null) {
+                byte ldModeByte = mapQualityLevel(ldModeString);
+                pictureParams.add(PictureParameter.LdMode(ldModeByte));
+            }
             params.remove(PictureQuality.PARAMETER_LD_MODE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_GAIN)) {
@@ -767,8 +920,44 @@
             params.remove(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH);
         }
         if (params.containsKey(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)) {
-            pictureParams.add(PictureParameter.pictureQualityEventType(
-                    (byte) params.getInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)));
+            String pictureQualityEventTypeString = params.getString(
+                    PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE);
+            if (pictureQualityEventTypeString != null) {
+                byte pictureQualityEventTypeByte;
+                switch (pictureQualityEventTypeString) {
+                    case "NONE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.NONE;
+                        break;
+                    case "BBD_RESULT":
+                        pictureQualityEventTypeByte = PictureQualityEventType.BBD_RESULT;
+                        break;
+                    case "VIDEO_DELAY_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.VIDEO_DELAY_CHANGE;
+                        break;
+                    case "CAPTUREPOINT_INFO_CHANGE":
+                        pictureQualityEventTypeByte =
+                                PictureQualityEventType.CAPTUREPOINT_INFO_CHANGE;
+                        break;
+                    case "VIDEOPATH_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.VIDEOPATH_CHANGE;
+                        break;
+                    case "EXTRA_FRAME_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.EXTRA_FRAME_CHANGE;
+                        break;
+                    case "DOLBY_IQ_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.DOLBY_IQ_CHANGE;
+                        break;
+                    case "DOLBY_APO_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.DOLBY_APO_CHANGE;
+                        break;
+                    default:
+                        pictureQualityEventTypeByte = PictureQualityEventType.NONE;
+                        Log.e("PictureParams", "Invalid event type string: "
+                                + pictureQualityEventTypeString);
+                }
+                pictureParams.add(
+                        PictureParameter.pictureQualityEventType(pictureQualityEventTypeByte));
+            }
             params.remove(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE);
         }
         return pictureParams.toArray(new PictureParameter[0]);
@@ -1657,6 +1846,19 @@
         return colIndex != -1 ? cursor.getString(colIndex) : null;
     }
 
+    private static byte mapQualityLevel(String qualityLevel) {
+        return switch (qualityLevel) {
+            case MediaQualityContract.LEVEL_OFF -> QualityLevel.OFF;
+            case MediaQualityContract.LEVEL_LOW -> QualityLevel.LOW;
+            case MediaQualityContract.LEVEL_MEDIUM -> QualityLevel.MEDIUM;
+            case MediaQualityContract.LEVEL_HIGH -> QualityLevel.HIGH;
+            default -> {
+                Log.e("PictureParams", "Invalid noise_reduction string: " + qualityLevel);
+                yield QualityLevel.OFF;
+            }
+        };
+    }
+
     private MediaQualityUtils() {
 
     }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index fcd1452..2744721 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -311,6 +311,7 @@
     static final int SHORT_PRESS_POWER_LOCK_OR_SLEEP = 6;
     static final int SHORT_PRESS_POWER_DREAM_OR_SLEEP = 7;
     static final int SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP = 8;
+    static final int SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP = 9;
 
     // must match: config_LongPressOnPowerBehavior in config.xml
     // The config value can be overridden using Settings.Global.POWER_BUTTON_LONG_PRESS
@@ -1234,8 +1235,10 @@
                     break;
                 }
                 case SHORT_PRESS_POWER_DREAM_OR_SLEEP: {
-                    attemptToDreamFromShortPowerButtonPress(
-                            true,
+                    attemptToDreamOrAwakeFromShortPowerButtonPress(
+                            /* isScreenOn */ true,
+                            /* awakeWhenDream */ false,
+                            /* noDreamAction */
                             () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
                     break;
                 }
@@ -1269,13 +1272,22 @@
                         lockNow(options);
                     } else {
                         // If the hub cannot be run, attempt to dream instead.
-                        attemptToDreamFromShortPowerButtonPress(
+                        attemptToDreamOrAwakeFromShortPowerButtonPress(
                                 /* isScreenOn */ true,
+                                /* awakeWhenDream */ false,
                                 /* noDreamAction */
                                 () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
                     }
                     break;
                 }
+                case SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP: {
+                    attemptToDreamOrAwakeFromShortPowerButtonPress(
+                            /* isScreenOn */ true,
+                            /* awakeWhenDream */ true,
+                            /* noDreamAction */
+                            () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
+                    break;
+                }
             }
         }
     }
@@ -1319,15 +1331,18 @@
     }
 
     /**
-     * Attempt to dream from a power button press.
+     * Attempt to dream, awake or sleep from a power button press.
      *
      * @param isScreenOn Whether the screen is currently on.
+     * @param awakeWhenDream When it's set to {@code true}, awake the device from dreaming.
+     *        Otherwise, go to sleep.
      * @param noDreamAction The action to perform if dreaming is not possible.
      */
-    private void attemptToDreamFromShortPowerButtonPress(
-            boolean isScreenOn, Runnable noDreamAction) {
+    private void attemptToDreamOrAwakeFromShortPowerButtonPress(
+            boolean isScreenOn, boolean awakeWhenDream, Runnable noDreamAction) {
         if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP
-                && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) {
+                && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP
+                && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP) {
             // If the power button behavior isn't one that should be able to trigger the dream, give
             // up.
             noDreamAction.run();
@@ -1335,9 +1350,24 @@
         }
 
         final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
-        if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) {
-            Slog.d(TAG, "Can't start dreaming when attempting to dream from short power"
-                    + " press (isScreenOn=" + isScreenOn + ")");
+        if (dreamManagerInternal == null) {
+            Slog.d(TAG,
+                    "Can't access dream manager dreaming when attempting to start or stop dream "
+                    + "from short power press (isScreenOn="
+                            + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")");
+            noDreamAction.run();
+            return;
+        }
+
+        if (!dreamManagerInternal.canStartDreaming(isScreenOn)) {
+            if (awakeWhenDream && dreamManagerInternal.isDreaming()) {
+                dreamManagerInternal.stopDream(false /*immediate*/, "short press power" /*reason*/);
+                return;
+            }
+            Slog.d(TAG,
+                    "Can't start dreaming and the device is not dreaming when attempting to start "
+                    + "or stop dream from short power press (isScreenOn="
+                            + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")");
             noDreamAction.run();
             return;
         }
@@ -2312,6 +2342,10 @@
             return ActivityManager.getService();
         }
 
+        LockPatternUtils getLockPatternUtils() {
+            return new LockPatternUtils(mContext);
+        }
+
         ButtonOverridePermissionChecker getButtonOverridePermissionChecker() {
             return new ButtonOverridePermissionChecker();
         }
@@ -2360,7 +2394,7 @@
         mAccessibilityShortcutController = injector.getAccessibilityShortcutController(
                 mContext, new Handler(), mCurrentUserId);
         mGlobalActionsFactory = injector.getGlobalActionsFactory();
-        mLockPatternUtils = new LockPatternUtils(mContext);
+        mLockPatternUtils = injector.getLockPatternUtils();
         mLogger = new MetricsLogger();
 
         Resources res = mContext.getResources();
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index ac4aac6..11edb93 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -383,7 +383,9 @@
                     /* api_name */
                     initialPhaseMetric.getApiName(),
                     /* primary_candidates_indicated */
-                    candidatePrimaryProviderList
+                    candidatePrimaryProviderList,
+                    /* api_prepared */
+                    initialPhaseMetric.hasApiUsedPrepareFlow()
             );
         } catch (Exception e) {
             Slog.w(TAG, "Unexpected error during candidate provider uid metric emit: " + e);
@@ -442,7 +444,9 @@
                     /* autofill_session_id */
                     initialPhaseMetric.getAutofillSessionId(),
                     /* autofill_request_id */
-                    initialPhaseMetric.getAutofillRequestId()
+                    initialPhaseMetric.getAutofillRequestId(),
+                    /* api_prepared */
+                    initialPhaseMetric.hasApiUsedPrepareFlow()
             );
         } catch (Exception e) {
             Slog.w(TAG, "Unexpected error during initial metric emit: " + e);
diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
index d60807c..2d4360e 100644
--- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
@@ -27,6 +27,7 @@
 import android.credentials.IGetCredentialCallback;
 import android.credentials.IPrepareGetCredentialCallback;
 import android.credentials.PrepareGetCredentialResponseInternal;
+import android.credentials.flags.Flags;
 import android.credentials.selection.GetCredentialProviderData;
 import android.credentials.selection.ProviderData;
 import android.credentials.selection.RequestInfo;
@@ -60,7 +61,12 @@
         int numTypes = (request.getCredentialOptions().stream()
                 .map(CredentialOption::getType).collect(
                         Collectors.toSet())).size(); // Dedupe type strings
-        mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
+        if (!Flags.fixMetricDuplicationEmits()) {
+            mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
+        } else {
+            mRequestSessionMetric.collectGetFlowInitialMetricInfo(request,
+                    /*isApiPrepared=*/ true);
+        }
         mPrepareGetCredentialCallback = prepareGetCredentialCallback;
 
         Slog.i(TAG, "PrepareGetRequestSession constructed.");
diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
index 8a4e86c..811b97a 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
@@ -55,6 +55,9 @@
     // The request id of autofill if the request is from autofill, defaults to -1
     private int mAutofillRequestId = -1;
 
+    // Indicates if this API call used the prepare flow, defaults to false
+    private boolean mApiUsedPrepareFlow = false;
+
 
     public InitialPhaseMetric(int sessionIdTrackOne) {
         mSessionIdCaller = sessionIdTrackOne;
@@ -173,4 +176,17 @@
     public int[] getUniqueRequestCounts() {
         return mRequestCounts.values().stream().mapToInt(Integer::intValue).toArray();
     }
+
+    /* ------ API Prepared ------ */
+
+    public void setApiUsedPrepareFlow(boolean apiUsedPrepareFlow) {
+        mApiUsedPrepareFlow = apiUsedPrepareFlow;
+    }
+
+    /**
+     * @return a boolean indicating if this API call utilized a prepare flow
+     */
+    public boolean hasApiUsedPrepareFlow() {
+        return mApiUsedPrepareFlow;
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
index 619a568..dc1747f 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
@@ -225,6 +225,22 @@
     }
 
     /**
+     * Collects initializations for Get flow metrics.
+     *
+     * @param request the get credential request containing information to parse for metrics
+     * @param isApiPrepared indicates this API flow utilized the 'prepare' flow
+     */
+    public void collectGetFlowInitialMetricInfo(GetCredentialRequest request,
+            boolean isApiPrepared) {
+        try {
+            collectGetFlowInitialMetricInfo(request);
+            mInitialPhaseMetric.setApiUsedPrepareFlow(isApiPrepared);
+        } catch (Exception e) {
+            Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e);
+        }
+    }
+
+    /**
      * During browsing, where multiple entries can be selected, this collects the browsing phase
      * metric information. This is emitted together with the final phase, and the recursive path
      * with authentication entries, which may occur in rare circumstances, are captured.
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index 232bb83..5a140d5 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -1753,6 +1753,13 @@
         }
         val appIdPermissionFlags = newState.mutateUserState(userId)!!.mutateAppIdPermissionFlags()
         val permissionFlags = appIdPermissionFlags.mutateOrPut(appId) { MutableIndexedMap() }
+        // for debugging possible races TODO(b/401768134)
+        oldState.userStates[userId]?.appIdPermissionFlags[appId]?.map?.let {
+            if (permissionFlags.map === it) {
+                throw IllegalStateException("Unexpected sharing between old/new state")
+            }
+        }
+
         permissionFlags.putWithDefault(permissionName, newFlags, 0)
         if (permissionFlags.isEmpty()) {
             appIdPermissionFlags -= appId
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index 99c922c..df77866 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -25,6 +25,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -866,6 +867,23 @@
 
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+    public void scrollPanelController_directionalButtonsHideIndicator() {
+        injectFakeMouseActionHoverMoveEvent();
+
+        // Create a spy on the real object to verify method calls.
+        AutoclickIndicatorView spyIndicatorView = spy(mController.mAutoclickIndicatorView);
+        mController.mAutoclickIndicatorView = spyIndicatorView;
+
+        // Simulate hover on direction button.
+        mController.mScrollPanelController.onHoverButtonChange(
+                AutoclickScrollPanel.DIRECTION_UP, true);
+
+        // Verify clearIndicator was called.
+        verify(spyIndicatorView).clearIndicator();
+    }
+
+    @Test
+    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() {
         MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
         mController.setNext(motionEventCaptor);
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java
new file mode 100644
index 0000000..f252a98
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2025 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.server.audio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.INativeAudioVolumeGroupCallback;
+import android.media.audio.common.AudioVolumeGroupChangeEvent;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioVolumeChangeHandlerTest {
+    private static final long DEFAULT_TIMEOUT_MS = 1000;
+
+    private AudioSystemAdapter mSpyAudioSystem;
+
+    AudioVolumeChangeHandler mAudioVolumeChangedHandler;
+
+    private final IAudioVolumeChangeDispatcher.Stub mMockDispatcher =
+            mock(IAudioVolumeChangeDispatcher.Stub.class);
+
+    @Before
+    public void setUp() {
+        mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+        when(mMockDispatcher.asBinder()).thenReturn(mock(IBinder.class));
+        mAudioVolumeChangedHandler = new AudioVolumeChangeHandler(mSpyAudioSystem);
+    }
+
+    @Test
+    public void registerListener_withInvalidCallback() {
+        IAudioVolumeChangeDispatcher.Stub nullCb = null;
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            mAudioVolumeChangedHandler.registerListener(nullCb);
+        });
+
+        assertWithMessage("Exception for invalid registration").that(thrown).hasMessageThat()
+                .contains("Volume group callback");
+    }
+
+    @Test
+    public void unregisterListener_withInvalidCallback() {
+        IAudioVolumeChangeDispatcher.Stub nullCb = null;
+        mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            mAudioVolumeChangedHandler.unregisterListener(nullCb);
+        });
+
+        assertWithMessage("Exception for invalid un-registration").that(thrown).hasMessageThat()
+                .contains("Volume group callback");
+    }
+
+    @Test
+    public void registerListener() {
+        mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+
+        verify(mSpyAudioSystem).registerAudioVolumeGroupCallback(any());
+    }
+
+    @Test
+    public void onAudioVolumeGroupChanged() throws Exception {
+        mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+        AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent();
+        volEvent.groupId = 666;
+        volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY;
+
+        captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent);
+
+        verify(mMockDispatcher,  timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged(
+                eq(volEvent.groupId), eq(volEvent.flags));
+    }
+
+    @Test
+    public void onAudioVolumeGroupChanged_withMultipleCallback() throws Exception {
+        int callbackCount = 10;
+        List<IAudioVolumeChangeDispatcher.Stub> validCbs =
+                new ArrayList<IAudioVolumeChangeDispatcher.Stub>();
+        for (int i = 0; i < callbackCount; i++) {
+            IAudioVolumeChangeDispatcher.Stub cb = mock(IAudioVolumeChangeDispatcher.Stub.class);
+            when(cb.asBinder()).thenReturn(mock(IBinder.class));
+            validCbs.add(cb);
+        }
+        for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) {
+            mAudioVolumeChangedHandler.registerListener(cb);
+        }
+        AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent();
+        volEvent.groupId = 666;
+        volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY;
+        captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent);
+
+        for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) {
+            verify(cb,  timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged(
+                    eq(volEvent.groupId), eq(volEvent.flags));
+        }
+    }
+
+    private INativeAudioVolumeGroupCallback captureRegisteredNativeCallback() {
+        ArgumentCaptor<INativeAudioVolumeGroupCallback> nativeAudioVolumeGroupCallbackCaptor =
+                ArgumentCaptor.forClass(INativeAudioVolumeGroupCallback.class);
+        verify(mSpyAudioSystem, timeout(DEFAULT_TIMEOUT_MS))
+                .registerAudioVolumeGroupCallback(nativeAudioVolumeGroupCallbackCaptor.capture());
+        return nativeAudioVolumeGroupCallbackCaptor.getValue();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
index 43b1ec3..87cd156 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
@@ -19,7 +19,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
@@ -29,6 +31,7 @@
 import android.content.Context;
 import android.hardware.contexthub.EndpointInfo;
 import android.hardware.contexthub.ErrorCode;
+import android.hardware.contexthub.HubEndpoint;
 import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier;
 import android.hardware.contexthub.HubMessage;
@@ -385,6 +388,49 @@
         unregisterExampleEndpoint(endpoint);
     }
 
+    @Test
+    public void testUnreliableMessageFailureClosesSession() throws RemoteException {
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+        int sessionId = openTestSession(endpoint);
+
+        doThrow(new RemoteException("Intended exception in test"))
+                .when(mMockCallback)
+                .onMessageReceived(anyInt(), any(HubMessage.class));
+        mEndpointManager.onMessageReceived(sessionId, SAMPLE_UNRELIABLE_MESSAGE);
+        ArgumentCaptor<HubMessage> messageCaptor = ArgumentCaptor.forClass(HubMessage.class);
+        verify(mMockCallback).onMessageReceived(eq(sessionId), messageCaptor.capture());
+        assertThat(messageCaptor.getValue()).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE);
+
+        verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.UNSPECIFIED);
+        verify(mMockCallback).onSessionClosed(sessionId, HubEndpoint.REASON_FAILURE);
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+
+        unregisterExampleEndpoint(endpoint);
+    }
+
+    @Test
+    public void testSendUnreliableMessageFailureClosesSession() throws RemoteException {
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+        int sessionId = openTestSession(endpoint);
+
+        doThrow(new RemoteException("Intended exception in test"))
+                .when(mMockEndpointCommunications)
+                .sendMessageToEndpoint(anyInt(), any(Message.class));
+        endpoint.sendMessage(sessionId, SAMPLE_UNRELIABLE_MESSAGE, /* callback= */ null);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+        verify(mMockEndpointCommunications)
+                .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture());
+        Message message = messageCaptor.getValue();
+        assertThat(message.type).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE.getMessageType());
+        assertThat(message.content).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE.getMessageBody());
+
+        verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.UNSPECIFIED);
+        verify(mMockCallback).onSessionClosed(sessionId, HubEndpoint.REASON_FAILURE);
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+
+        unregisterExampleEndpoint(endpoint);
+    }
+
     /** A helper method to create a session and validates reliable message sending. */
     private void testMessageTransactionInternal(
             IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException {
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index 6822968..f3d5e39 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -35,6 +35,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 import static com.android.server.policy.PhoneWindowManager.EXTRA_TRIGGER_HUB;
+import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP;
 import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -64,6 +65,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.test.LocalServiceKeeperRule;
+import com.android.internal.widget.LockPatternUtils;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.policy.keyguard.KeyguardServiceDelegate;
@@ -120,6 +122,8 @@
     private DisplayPolicy mDisplayPolicy;
     @Mock
     private KeyguardServiceDelegate mKeyguardServiceDelegate;
+    @Mock
+    private LockPatternUtils mLockPatternUtils;
 
     @Before
     public void setUp() {
@@ -254,6 +258,7 @@
     @Test
     public void powerPress_hubOrDreamOrSleep_goesToSleepFromDream() {
         when(mDisplayPolicy.isAwake()).thenReturn(true);
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
         initPhoneWindowManager();
 
         // Set power button behavior.
@@ -275,6 +280,7 @@
     @Test
     public void powerPress_hubOrDreamOrSleep_hubAvailableLocks() {
         when(mDisplayPolicy.isAwake()).thenReturn(true);
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
         mContext.getTestablePermissions().setPermission(android.Manifest.permission.DEVICE_POWER,
                 PERMISSION_GRANTED);
         initPhoneWindowManager();
@@ -303,6 +309,7 @@
     @Test
     public void powerPress_hubOrDreamOrSleep_hubNotAvailableDreams() {
         when(mDisplayPolicy.isAwake()).thenReturn(true);
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
         initPhoneWindowManager();
 
         // Set power button behavior.
@@ -323,6 +330,77 @@
         verify(mDreamManagerInternal).requestDream();
     }
 
+    @Test
+    public void powerPress_dreamOrAwakeOrSleep_awakeFromDream() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS,
+                SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Can not dream when device is dreaming.
+        when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false);
+        // Device is dreaming.
+        when(mDreamManagerInternal.isDreaming()).thenReturn(true);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Dream is stopped.
+        verify(mDreamManagerInternal)
+                .stopDream(false /*immediate*/, "short press power" /*reason*/);
+    }
+
+    @Test
+    public void powerPress_dreamOrAwakeOrSleep_canNotDreamGoToSleep() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS,
+                SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Can not dream for other reasons.
+        when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false);
+        // Device is not dreaming.
+        when(mDreamManagerInternal.isDreaming()).thenReturn(false);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Device goes to sleep.
+        verify(mPowerManager).goToSleep(eventTime, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0);
+    }
+
+    @Test
+    public void powerPress_dreamOrAwakeOrSleep_dreamFromActive() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS,
+                SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Can dream when active.
+        when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(true);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Dream is requested.
+        verify(mDreamManagerInternal).requestDream();
+    }
+
     private void initPhoneWindowManager() {
         mPhoneWindowManager.mDefaultDisplayPolicy = mDisplayPolicy;
         mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
@@ -346,6 +424,11 @@
             return mKeyguardServiceDelegate;
         }
 
+        @Override
+        LockPatternUtils getLockPatternUtils() {
+            return mLockPatternUtils;
+        }
+
         /**
          * {@code WindowWakeUpPolicy} registers a local service in its constructor, easier to just
          * mock it out so we don't have to unregister it after every test.
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 1491510..19574fd 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -11493,17 +11493,17 @@
                         + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"});
         PersistableBundle auto_data_switch_rat_signal_score_string_bundle = new PersistableBundle();
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "NR_SA_MMWAVE", new int[]{10000, 13227, 16000, 18488, 20017});
+                "NR_SA_MMWAVE", new int[]{6300, 10227, 16000, 18488, 19017});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "NR_NSA_MMWAVE", new int[]{8000, 10227, 12488, 15017, 15278});
+                "NR_NSA_MMWAVE", new int[]{5700, 9227, 12488, 13517, 15978});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
                 "LTE", new int[]{3731, 5965, 8618, 11179, 13384});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "LTE_CA", new int[]{3831, 6065, 8718, 11379, 13484});
+                "LTE_CA", new int[]{3831, 6065, 8718, 11379, 14484});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "NR_SA", new int[]{5288, 6795, 6955, 7562, 9713});
+                "NR_SA", new int[]{2288, 6795, 6955, 7562, 15484});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "NR_NSA", new int[]{5463, 6827, 8029, 9007, 9428});
+                "NR_NSA", new int[]{2463, 6827, 8029, 9007, 15884});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
                 "UMTS", new int[]{100, 169, 183, 192, 300});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto
index f4735a2..380c5f2 100644
--- a/tools/aapt2/ResourcesInternal.proto
+++ b/tools/aapt2/ResourcesInternal.proto
@@ -50,8 +50,11 @@
   // Any symbols this file auto-generates/exports (eg. @+id/foo in an XML file).
   repeated Symbol exported_symbol = 5;
 
-  // The status of the flag the file is behind if any
+  // The status of the read only flag the file is behind if any
   uint32 flag_status = 6;
   bool flag_negated = 7;
   string flag_name = 8;
+
+  // Whether the file uses read/write feature flags
+  bool uses_readwrite_feature_flags = 9;
 }
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index a5e18d35..3b4f542 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -407,6 +407,45 @@
   return true;
 }
 
+class FindReadWriteFlagsVisitor : public xml::Visitor {
+ public:
+  FindReadWriteFlagsVisitor(const FeatureFlagValues& feature_flag_values)
+      : feature_flag_values_(feature_flag_values) {
+  }
+
+  void Visit(xml::Element* node) override {
+    if (had_flags_) {
+      return;
+    }
+    auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
+    if (attr != nullptr) {
+      std::string_view flag_name = util::TrimWhitespace(attr->value);
+      if (flag_name.starts_with('!')) {
+        flag_name = flag_name.substr(1);
+      }
+      if (auto it = feature_flag_values_.find(flag_name); it != feature_flag_values_.end()) {
+        if (!it->second.read_only) {
+          had_flags_ = true;
+          return;
+        }
+      } else {
+        // Flag not passed to aapt2, must evaluate at runtime
+        had_flags_ = true;
+        return;
+      }
+    }
+    VisitChildren(node);
+  }
+
+  bool HadFlags() const {
+    return had_flags_;
+  }
+
+ private:
+  bool had_flags_ = false;
+  const FeatureFlagValues& feature_flag_values_;
+};
+
 static bool CompileXml(IAaptContext* context, const CompileOptions& options,
                        const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
                        const std::string& output_path) {
@@ -436,6 +475,10 @@
   xmlres->file.type = ResourceFile::Type::kProtoXml;
   xmlres->file.flag = ParseFlag(path_data.flag_name);
 
+  FindReadWriteFlagsVisitor visitor(options.feature_flag_values);
+  xmlres->root->Accept(&visitor);
+  xmlres->file.uses_readwrite_feature_flags = visitor.HadFlags();
+
   if (xmlres->file.flag) {
     std::string error;
     auto flag_status = GetFlagStatus(xmlres->file.flag, options.feature_flag_values, &error);
diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h
index 9452e58..5576ec0 100644
--- a/tools/aapt2/cmd/Convert.h
+++ b/tools/aapt2/cmd/Convert.h
@@ -38,14 +38,14 @@
         "--enable-sparse-encoding",
         "Enables encoding sparse entries using a binary search tree.\n"
         "This decreases APK size at the cost of resource retrieval performance.\n"
-        "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
-        "the APK is O+",
+        "Only applies sparse encoding if minSdk of the APK is >= 32",
         &enable_sparse_encoding_);
-    AddOptionalSwitch("--force-sparse-encoding",
-                      "Enables encoding sparse entries using a binary search tree.\n"
-                      "This decreases APK size at the cost of resource retrieval performance.\n"
-                      "Applies sparse encoding to all resources regardless of minSdk.",
-                      &force_sparse_encoding_);
+    AddOptionalSwitch(
+        "--force-sparse-encoding",
+        "Enables encoding sparse entries using a binary search tree.\n"
+        "This decreases APK size at the cost of resource retrieval performance.\n"
+        "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set",
+        &force_sparse_encoding_);
     AddOptionalSwitch(
         "--enable-compact-entries",
         "This decreases APK size by using compact resource entries for simple data types.",
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 755dbb6..0e18ee2 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -615,6 +615,8 @@
             file_op.xml_to_flatten->file.source = file_ref->GetSource();
             file_op.xml_to_flatten->file.name =
                 ResourceName(pkg->name, type->named_type, entry->name);
+            file_op.xml_to_flatten->file.uses_readwrite_feature_flags =
+                config_value->uses_readwrite_feature_flags;
           }
 
           // NOTE(adamlesinski): Explicitly construct a StringPiece here, or
@@ -647,6 +649,17 @@
             }
           }
 
+          FeatureFlagsFilterOptions flags_filter_options;
+          // Don't fail on unrecognized flags or flags without values as these flags might be
+          // defined and have a value by the time they are evaluated at runtime.
+          flags_filter_options.fail_on_unrecognized_flags = false;
+          flags_filter_options.flags_must_have_value = false;
+          flags_filter_options.remove_disabled_elements = true;
+          FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
+          if (!flags_filter.Consume(context_, file_op.xml_to_flatten.get())) {
+            return 1;
+          }
+
           std::vector<std::unique_ptr<xml::XmlResource>> versioned_docs =
               LinkAndVersionXmlFile(table, &file_op);
           if (versioned_docs.empty()) {
@@ -673,6 +686,7 @@
 
               // Update the output format of this XML file.
               file_ref->type = XmlFileTypeForOutputFormat(options_.output_format);
+
               bool result = table->AddResource(
                   NewResourceBuilder(file.name)
                       .SetValue(std::move(file_ref), file.config)
@@ -685,14 +699,6 @@
               }
             }
 
-            FeatureFlagsFilterOptions flags_filter_options;
-            flags_filter_options.fail_on_unrecognized_flags = false;
-            flags_filter_options.flags_must_have_value = false;
-            FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
-            if (!flags_filter.Consume(context_, doc.get())) {
-              return 1;
-            }
-
             error |= !FlattenXml(context_, *doc, dst_path, options_.keep_raw_values,
                                  false /*utf16*/, options_.output_format, archive_writer);
           }
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index 9779788..54a8c86 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -164,9 +164,12 @@
     AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n"
             "defaults. Use this only when building runtime resource overlay packages.",
         &options_.no_resource_removal);
-    AddOptionalSwitch("--enable-sparse-encoding",
-                      "This decreases APK size at the cost of resource retrieval performance.",
-                      &options_.use_sparse_encoding);
+    AddOptionalSwitch(
+        "--enable-sparse-encoding",
+        "Enables encoding sparse entries using a binary search tree.\n"
+        "This decreases APK size at the cost of resource retrieval performance.\n"
+        "Only applies sparse encoding if minSdk of the APK is >= 32",
+        &options_.use_sparse_encoding);
     AddOptionalSwitch("--enable-compact-entries",
         "This decreases APK size by using compact resource entries for simple data types.",
         &options_.table_flattener_options.use_compact_entries);
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index 012b0f2..a8f547e 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -108,14 +108,14 @@
         "--enable-sparse-encoding",
         "Enables encoding sparse entries using a binary search tree.\n"
         "This decreases APK size at the cost of resource retrieval performance.\n"
-        "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
-        "the APK is O+",
+        "Only applies sparse encoding if minSdk of the APK is >= 32",
         &options_.enable_sparse_encoding);
-    AddOptionalSwitch("--force-sparse-encoding",
-                      "Enables encoding sparse entries using a binary search tree.\n"
-                      "This decreases APK size at the cost of resource retrieval performance.\n"
-                      "Applies sparse encoding to all resources regardless of minSdk.",
-                      &options_.force_sparse_encoding);
+    AddOptionalSwitch(
+        "--force-sparse-encoding",
+        "Enables encoding sparse entries using a binary search tree.\n"
+        "This decreases APK size at the cost of resource retrieval performance.\n"
+        "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set",
+        &options_.force_sparse_encoding);
     AddOptionalSwitch(
         "--enable-compact-entries",
         "This decreases APK size by using compact resource entries for simple data types.",
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 50144ae..d19c9f2 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -197,13 +197,16 @@
     bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled ||
                          sparse_entries_ == SparseEntriesMode::Forced;
 
-    if (sparse_entries_ == SparseEntriesMode::Forced ||
-        (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) {
-      // Sparse encode if forced or sdk version is not set in context and config.
-    } else {
-      // Otherwise, only sparse encode if the entries will be read on platforms S_V2+.
-      sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2);
-    }
+    // Only sparse encode if the entries will be read on platforms S_V2+. Sparse encoding
+    // is not supported on older platforms (b/197642721, b/197976367).
+    //
+    // We also allow sparse encoding for minSdk is 0 (not set) if sparse encoding is forced,
+    // in order to support Bundletool's usage of aapt2 where minSdk is not set in splits.
+    bool meets_min_sdk_requirement_for_sparse_encoding =
+        (context_->GetMinSdkVersion() >= SDK_S_V2) ||
+        (context_->GetMinSdkVersion() == 0 && sparse_entries_ == SparseEntriesMode::Forced);
+
+    sparse_encode = sparse_encode && meets_min_sdk_requirement_for_sparse_encoding;
 
     // Only sparse encode if the offsets are representable in 2 bytes.
     sparse_encode = sparse_encode && short_offsets;
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 9156b96..0e8aae1 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -15,6 +15,7 @@
  */
 
 #include "format/binary/TableFlattener.h"
+#include <string>
 
 #include "android-base/stringprintf.h"
 #include "androidfw/TypeWrappers.h"
@@ -326,6 +327,28 @@
   return table;
 }
 
+static void CheckSparseEntries(IAaptContext* context, const ConfigDescription& sparse_config,
+                               const std::string& sparse_contents) {
+  ResourceTable sparse_table;
+  BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
+                              sparse_contents.data(), sparse_contents.size());
+  ASSERT_TRUE(parser.Parse());
+
+  auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
+                                                        sparse_config);
+  ASSERT_THAT(value, NotNull());
+  EXPECT_EQ(0u, value->value.data);
+
+  ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
+                                                       sparse_config),
+              IsNull());
+
+  value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
+                                                   sparse_config);
+  ASSERT_THAT(value, NotNull());
+  EXPECT_EQ(4u, value->value.data);
+}
+
 TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder()
                                               .SetCompilationPackage("android")
@@ -347,29 +370,56 @@
 
   EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
 
-  // Attempt to parse the sparse contents.
-
-  ResourceTable sparse_table;
-  BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
-                              sparse_contents.data(), sparse_contents.size());
-  ASSERT_TRUE(parser.Parse());
-
-  auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
-                                                        sparse_config);
-  ASSERT_THAT(value, NotNull());
-  EXPECT_EQ(0u, value->value.data);
-
-  ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
-                                                       sparse_config),
-              IsNull());
-
-  value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
-                                                   sparse_config);
-  ASSERT_THAT(value, NotNull());
-  EXPECT_EQ(4u, value->value.data);
+  CheckSparseEntries(context.get(), sparse_config, sparse_contents);
 }
 
-TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) {
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2AndForced) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder()
+                                              .SetCompilationPackage("android")
+                                              .SetPackageId(0x01)
+                                              .SetMinSdkVersion(SDK_S_V2)
+                                              .Build();
+
+  const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+  auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
+
+  TableFlattenerOptions options;
+  options.sparse_entries = SparseEntriesMode::Forced;
+
+  std::string no_sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+
+  std::string sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
+  EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+
+  CheckSparseEntries(context.get(), sparse_config, sparse_contents);
+}
+
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder()
+                                              .SetCompilationPackage("android")
+                                              .SetPackageId(0x01)
+                                              .SetMinSdkVersion(SDK_LOLLIPOP)
+                                              .Build();
+
+  const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+  auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
+
+  TableFlattenerOptions options;
+  options.sparse_entries = SparseEntriesMode::Enabled;
+
+  std::string no_sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+
+  std::string sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
+  EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
+}
+
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndConfigSdkVersionSV2) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder()
                                               .SetCompilationPackage("android")
                                               .SetPackageId(0x01)
@@ -391,7 +441,7 @@
   EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
 }
 
-TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) {
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndForced) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder()
                                               .SetCompilationPackage("android")
                                               .SetPackageId(0x01)
@@ -410,7 +460,7 @@
   std::string sparse_contents;
   ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
 
-  EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+  EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
 }
 
 TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) {
@@ -429,28 +479,28 @@
   std::string sparse_contents;
   ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
 
+  EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
+}
+
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSetAndForced) {
+  std::unique_ptr<IAaptContext> context =
+      test::ContextBuilder().SetCompilationPackage("android").SetPackageId(0x01).Build();
+
+  const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+  auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
+
+  TableFlattenerOptions options;
+  options.sparse_entries = SparseEntriesMode::Forced;
+
+  std::string no_sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+
+  std::string sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
   EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
 
-  // Attempt to parse the sparse contents.
-
-  ResourceTable sparse_table;
-  BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
-                              sparse_contents.data(), sparse_contents.size());
-  ASSERT_TRUE(parser.Parse());
-
-  auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
-                                                        sparse_config);
-  ASSERT_THAT(value, NotNull());
-  EXPECT_EQ(0u, value->value.data);
-
-  ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
-                                                       sparse_config),
-              IsNull());
-
-  value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
-                                                   sparse_config);
-  ASSERT_THAT(value, NotNull());
-  EXPECT_EQ(4u, value->value.data);
+  CheckSparseEntries(context.get(), sparse_config, sparse_contents);
 }
 
 TEST_F(TableFlattenerTest, DoNotUseSparseEntryForDenseConfig) {
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 91ec348..b893655 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -640,6 +640,7 @@
   out_file->name = name_ref.ToResourceName();
   out_file->source.path = pb_file.source_path();
   out_file->type = DeserializeFileReferenceTypeFromPb(pb_file.type());
+  out_file->uses_readwrite_feature_flags = pb_file.uses_readwrite_feature_flags();
 
   out_file->flag_status = (FlagStatus)pb_file.flag_status();
   if (!pb_file.flag_name().empty()) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index fcc77d5..da99c4f 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -767,6 +767,7 @@
     out_file->set_flag_negated(file.flag->negated);
     out_file->set_flag_name(file.flag->name);
   }
+  out_file->set_uses_readwrite_feature_flags(file.uses_readwrite_feature_flags);
 
   for (const SourcedResourceName& exported : file.exported_symbols) {
     pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol();
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index 47a71fe..4dcb850 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -226,9 +226,11 @@
       }
     }
   }
+
   ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set";
-  // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1
-  ASSERT_EQ(fields_flagged, 1);
+  // There should only be 2 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1, the
+  // three versions of the layout file that has flags
+  ASSERT_EQ(fields_flagged, 3);
 }
 
 }  // namespace aapt
diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp
index 8a3337c..626cae7 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp
@@ -35,10 +35,6 @@
     VisitChildren(node);
   }
 
-  bool HadFlags() const {
-    return had_flags_;
-  }
-
  private:
   bool FixupOrShouldRemove(const std::unique_ptr<xml::Node>& node) {
     if (auto* el = NodeCast<Element>(node.get())) {
@@ -47,7 +43,6 @@
         return false;
       }
 
-      had_flags_ = true;
       // This class assumes all flags are disabled so we want to remove any elements behind flags
       // unless the flag specification is negated. In the negated case we remove the featureFlag
       // attribute because we have already determined whether we are keeping the element or not.
@@ -62,56 +57,27 @@
 
     return false;
   }
-
-  bool had_flags_ = false;
-};
-
-// An xml visitor that goes through the a doc and determines if any elements are behind a flag.
-class FindFlagsVisitor : public xml::Visitor {
- public:
-  void Visit(xml::Element* node) override {
-    if (had_flags_) {
-      return;
-    }
-    auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
-    if (attr != nullptr) {
-      had_flags_ = true;
-      return;
-    }
-    VisitChildren(node);
-  }
-
-  bool HadFlags() const {
-    return had_flags_;
-  }
-
-  bool had_flags_ = false;
 };
 
 std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context,
                                                                             xml::XmlResource* doc) {
   std::vector<std::unique_ptr<xml::XmlResource>> docs;
-  if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) ||
-      (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) {
+  if (!doc->file.uses_readwrite_feature_flags) {
+    docs.push_back(doc->Clone());
+  } else if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) ||
+             (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) {
     // Support for read/write flags was added in baklava so if the doc will only get used on
     // baklava or later we can just return the original doc.
     docs.push_back(doc->Clone());
-    FindFlagsVisitor visitor;
-    doc->root->Accept(&visitor);
-    docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags();
   } else {
     auto preBaklavaVersion = doc->Clone();
     AllDisabledFlagsVisitor visitor;
     preBaklavaVersion->root->Accept(&visitor);
-    preBaklavaVersion->file.uses_readwrite_feature_flags = false;
     docs.push_back(std::move(preBaklavaVersion));
 
-    if (visitor.HadFlags()) {
-      auto baklavaVersion = doc->Clone();
-      baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
-      baklavaVersion->file.uses_readwrite_feature_flags = true;
-      docs.push_back(std::move(baklavaVersion));
-    }
+    auto baklavaVersion = doc->Clone();
+    baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
+    docs.push_back(std::move(baklavaVersion));
   }
   return docs;
 }
diff --git a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
index 0c1314f..0dc4642 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
@@ -101,6 +101,7 @@
         <TextView android:featureFlag="package.flag" /><TextView /><TextView />
       </LinearLayout>)");
   doc->file.config.sdkVersion = SDK_GINGERBREAD;
+  doc->file.uses_readwrite_feature_flags = true;
 
   FlaggedXmlVersioner versioner;
   auto results = versioner.Process(context_.get(), doc.get());
@@ -131,6 +132,7 @@
       <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
         <TextView android:featureFlag="package.flag" /><TextView /><TextView />
       </LinearLayout>)");
+  doc->file.uses_readwrite_feature_flags = true;
 
   FlaggedXmlVersioner versioner;
   auto results = versioner.Process(context_.get(), doc.get());
@@ -162,6 +164,7 @@
         <TextView android:featureFlag="!package.flag" /><TextView /><TextView />
       </LinearLayout>)");
   doc->file.config.sdkVersion = SDK_GINGERBREAD;
+  doc->file.uses_readwrite_feature_flags = true;
 
   FlaggedXmlVersioner versioner;
   auto results = versioner.Process(context_.get(), doc.get());
@@ -192,6 +195,7 @@
       <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
         <TextView android:featureFlag="!package.flag" /><TextView /><TextView />
       </LinearLayout>)");
+  doc->file.uses_readwrite_feature_flags = true;
 
   FlaggedXmlVersioner versioner;
   auto results = versioner.Process(context_.get(), doc.get());
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 1d4adc4..17f3323 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -295,6 +295,8 @@
           dst_config_value =
               dst_entry->FindOrCreateValue(src_config_value->config, src_config_value->product);
         }
+        dst_config_value->uses_readwrite_feature_flags |=
+            src_config_value->uses_readwrite_feature_flags;
 
         // Continue if we're taking the new resource.
         CloningValueTransformer cloner(&main_table_->string_pool);
@@ -378,12 +380,13 @@
   file_ref->file = file;
   file_ref->SetFlagStatus(file_desc.flag_status);
   file_ref->SetFlag(file_desc.flag);
-
   ResourceTablePackage* pkg = table.FindOrCreatePackage(file_desc.name.package);
-  pkg->FindOrCreateType(file_desc.name.type)
-      ->FindOrCreateEntry(file_desc.name.entry)
-      ->FindOrCreateValue(file_desc.config, {})
-      ->value = std::move(file_ref);
+  ResourceConfigValue* config_value = pkg->FindOrCreateType(file_desc.name.type)
+                                          ->FindOrCreateEntry(file_desc.name.entry)
+                                          ->FindOrCreateValue(file_desc.config, {});
+
+  config_value->value = std::move(file_ref);
+  config_value->uses_readwrite_feature_flags = file_desc.uses_readwrite_feature_flags;
 
   return DoMerge(file->GetSource(), pkg, false /*mangle*/, overlay /*overlay*/, true /*allow_new*/);
 }
diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md
index 6bdbaae..413f817 100644
--- a/tools/aapt2/readme.md
+++ b/tools/aapt2/readme.md
@@ -5,6 +5,10 @@
   2017. This README will be updated more frequently in the future.
 - Added a new flag `--no-compress-fonts`. This can significantly speed up loading fonts from APK
   assets, at the cost of increasing the storage size of the APK.
+- Changed the behavior of `--enable-sparse-encoding`. Sparse encoding is only applied if the
+  minSdkVersion is >= 32.
+- Changed the behavior of `--force-sparse-encoding`. Sparse encoding is only applied if the
+  minSdkVersion is >= 32 or is not set.
 
 ## Version 2.19
 - Added navigation resource type.