Merge "Update SurfaceEffect color tokens to allow use via Compose." into main
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index 29df80f..876274e 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -116,3 +116,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "additional_quota_for_system_installer"
+    namespace: "backstage_power"
+    description: "Offer additional quota for system installer"
+    bug: "398264531"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0298c1e6..251776e 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -2995,6 +2995,8 @@
             pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS,
                     Flags.startUserBeforeScheduledAlarms());
             pw.println();
+            pw.print(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND, Flags.acquireWakelockBeforeSend());
+            pw.println();
             pw.decreaseIndent();
             pw.println();
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 637c726..54d337e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -494,6 +494,9 @@
 
     private long mEjLimitAdditionSpecialMs = QcConstants.DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS;
 
+    private long mAllowedTimePeriodAdditionaInstallerMs =
+            QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS;
+
     /**
      * The period of time used to calculate expedited job sessions. Apps can only have expedited job
      * sessions totalling {@link #mEJLimitsMs}[bucket within this period of time (without factoring
@@ -1095,6 +1098,18 @@
         return baseLimitMs;
     }
 
+    private long getAllowedTimePerPeriodMsLocked(final int userId, @NonNull final String pkgName,
+            final int standbyBucket) {
+        final long baseLimitMs = mAllowedTimePerPeriodMs[standbyBucket];
+        if (Flags.adjustQuotaDefaultConstants()
+                && Flags.additionalQuotaForSystemInstaller()
+                && standbyBucket == EXEMPTED_INDEX
+                && mSystemInstallers.contains(userId, pkgName)) {
+            return baseLimitMs + mAllowedTimePeriodAdditionaInstallerMs;
+        }
+        return baseLimitMs;
+    }
+
     /**
      * Returns the amount of time, in milliseconds, until the package would have reached its
      * duration quota, assuming it has a job counting towards its quota the entire time. This takes
@@ -1112,25 +1127,26 @@
 
         List<TimedEvent> events = mTimingEvents.get(userId, packageName);
         final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+        final long allowedTimePerPeriodMs =
+                getAllowedTimePerPeriodMsLocked(userId, packageName, standbyBucket);
         if (events == null || events.size() == 0) {
             // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
             // essentially run until they reach the maximum limit.
-            if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
+            if (stats.windowSizeMs == allowedTimePerPeriodMs) {
                 return mMaxExecutionTimeMs;
             }
-            return mAllowedTimePerPeriodMs[standbyBucket];
+            return allowedTimePerPeriodMs;
         }
 
         final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
         final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
-        final long allowedTimePerPeriodMs = mAllowedTimePerPeriodMs[standbyBucket];
         final long allowedTimeRemainingMs = allowedTimePerPeriodMs - stats.executionTimeInWindowMs;
         final long maxExecutionTimeRemainingMs =
                 mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs;
 
         // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
         // essentially run until they reach the maximum limit.
-        if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
+        if (stats.windowSizeMs == allowedTimePerPeriodMs) {
             return calculateTimeUntilQuotaConsumedLocked(
                     events, startMaxElapsed, maxExecutionTimeRemainingMs);
         }
@@ -1270,7 +1286,8 @@
             appStats[standbyBucket] = stats;
         }
         if (refreshStatsIfOld) {
-            final long bucketAllowedTimeMs = mAllowedTimePerPeriodMs[standbyBucket];
+            final long bucketAllowedTimeMs =
+                    getAllowedTimePerPeriodMsLocked(userId, packageName, standbyBucket);
             final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
             final int jobCountLimit = mMaxBucketJobCounts[standbyBucket];
             final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket];
@@ -1845,9 +1862,10 @@
         final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats);
         final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats);
         final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName);
-
+        final long allowedTimePerPeriosMs =
+                getAllowedTimePerPeriodMsLocked(userId, packageName, standbyBucket);
         final boolean inRegularQuota =
-                stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs[standbyBucket]
+                stats.executionTimeInWindowMs < allowedTimePerPeriosMs
                         && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
                         && isUnderJobCountQuota
                         && isUnderTimingSessionCountQuota;
@@ -3037,6 +3055,9 @@
         static final String KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
                 QC_CONSTANT_PREFIX + "allowed_time_per_period_restricted_ms";
         @VisibleForTesting
+        static final String KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS =
+                QC_CONSTANT_PREFIX + "allowed_time_per_period_addition_installer_ms";
+        @VisibleForTesting
         static final String KEY_IN_QUOTA_BUFFER_MS =
                 QC_CONSTANT_PREFIX + "in_quota_buffer_ms";
         @VisibleForTesting
@@ -3169,6 +3190,8 @@
                 10 * 60 * 1000L; // 10 minutes
         private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
                 10 * 60 * 1000L; // 10 minutes
+        private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS =
+                10 * 60 * 1000L; // 10 minutes
         private static final long DEFAULT_IN_QUOTA_BUFFER_MS =
                 30 * 1000L; // 30 seconds
         // Legacy default window size for EXEMPTED bucket
@@ -3509,6 +3532,9 @@
          */
         public long EJ_LIMIT_ADDITION_INSTALLER_MS = DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS;
 
+        public long ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS =
+                DEFAULT_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS;
+
         /**
          * The period of time used to calculate expedited job sessions. Apps can only have expedited
          * job sessions totalling EJ_LIMIT_<bucket>_MS within this period of time (without factoring
@@ -3603,6 +3629,7 @@
                 case KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS:
                 case KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS:
                 case KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS:
+                case KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS:
                 case KEY_IN_QUOTA_BUFFER_MS:
                 case KEY_MAX_EXECUTION_TIME_MS:
                 case KEY_WINDOW_SIZE_ACTIVE_MS:
@@ -3847,7 +3874,7 @@
                     KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,
                     KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS, KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS,
                     KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS, KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
-                    KEY_IN_QUOTA_BUFFER_MS,
+                    KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS, KEY_IN_QUOTA_BUFFER_MS,
                     KEY_MAX_EXECUTION_TIME_MS,
                     KEY_WINDOW_SIZE_EXEMPTED_MS, KEY_WINDOW_SIZE_ACTIVE_MS,
                     KEY_WINDOW_SIZE_WORKING_MS,
@@ -3871,6 +3898,9 @@
             ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
                     properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
                             DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS);
+            ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS =
+                    properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+                            DEFAULT_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS);
             IN_QUOTA_BUFFER_MS = properties.getLong(KEY_IN_QUOTA_BUFFER_MS,
                     DEFAULT_IN_QUOTA_BUFFER_MS);
             MAX_EXECUTION_TIME_MS = properties.getLong(KEY_MAX_EXECUTION_TIME_MS,
@@ -3995,6 +4025,18 @@
                 mBucketPeriodsMs[RESTRICTED_INDEX] = newRestrictedPeriodMs;
                 mShouldReevaluateConstraints = true;
             }
+
+            if (Flags.additionalQuotaForSystemInstaller()) {
+                // The additions must be in the range
+                // [0 minutes, exempted window size - active limit].
+                long newAdditionInstallerMs = Math.max(0,
+                        Math.min(mBucketPeriodsMs[EXEMPTED_INDEX] - newAllowedTimeExemptedMs,
+                                ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS));
+                if (mAllowedTimePeriodAdditionaInstallerMs != newAdditionInstallerMs) {
+                    mAllowedTimePeriodAdditionaInstallerMs = newAdditionInstallerMs;
+                    mShouldReevaluateConstraints = true;
+                }
+            }
         }
 
         private void updateRateLimitingConstantsLocked() {
@@ -4159,6 +4201,8 @@
                     .println();
             pw.print(KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
                     ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS).println();
+            pw.print(KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+                    ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS).println();
             pw.print(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println();
             pw.print(KEY_WINDOW_SIZE_EXEMPTED_MS, WINDOW_SIZE_EXEMPTED_MS).println();
             pw.print(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println();
@@ -4335,6 +4379,11 @@
     }
 
     @VisibleForTesting
+    long getAllowedTimePeriodAdditionInstallerMs() {
+        return mAllowedTimePeriodAdditionaInstallerMs;
+    }
+
+    @VisibleForTesting
     long getEjLimitAdditionSpecialMs() {
         return mEjLimitAdditionSpecialMs;
     }
@@ -4435,6 +4484,8 @@
                 + ": " + Flags.enforceQuotaPolicyToFgsJobs());
         pw.println("    " + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS
                 + ": " + Flags.enforceQuotaPolicyToTopStartedJobs());
+        pw.println("    " + Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER
+                + ": " + Flags.additionalQuotaForSystemInstaller());
         pw.println();
 
         pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis());
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 458c171..248f191 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1651,9 +1651,65 @@
     /** @hide Similar to {@link OP_CONTROL_AUDIO}, but doesn't require capabilities. */
     public static final int OP_CONTROL_AUDIO_PARTIAL = AppOpEnums.APP_OP_CONTROL_AUDIO_PARTIAL;
 
+    /**
+     * Access coarse eye tracking data.
+     *
+     * @hide
+     */
+    public static final int OP_EYE_TRACKING_COARSE =
+            AppOpEnums.APP_OP_EYE_TRACKING_COARSE;
+
+    /**
+     * Access fine eye tracking data.
+     *
+     * @hide
+     */
+    public static final int OP_EYE_TRACKING_FINE =
+            AppOpEnums.APP_OP_EYE_TRACKING_FINE;
+
+    /**
+     * Access face tracking data.
+     *
+     * @hide
+     */
+    public static final int OP_FACE_TRACKING =
+            AppOpEnums.APP_OP_FACE_TRACKING;
+
+    /**
+     * Access hand tracking data.
+     *
+     * @hide
+     */
+    public static final int OP_HAND_TRACKING =
+            AppOpEnums.APP_OP_HAND_TRACKING;
+
+    /**
+     * Access head tracking data.
+     *
+     * @hide
+     */
+    public static final int OP_HEAD_TRACKING =
+            AppOpEnums.APP_OP_HEAD_TRACKING;
+
+    /**
+     * Access coarse scene tracking data.
+     *
+     * @hide
+     */
+    public static final int OP_SCENE_UNDERSTANDING_COARSE =
+            AppOpEnums.APP_OP_SCENE_UNDERSTANDING_COARSE;
+
+    /**
+     * Access fine scene tracking data.
+     *
+     * @hide
+     */
+    public static final int OP_SCENE_UNDERSTANDING_FINE =
+            AppOpEnums.APP_OP_SCENE_UNDERSTANDING_FINE;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 156;
+    public static final int _NUM_OP = 163;
 
     /**
      * All app ops represented as strings.
@@ -1813,6 +1869,13 @@
             OPSTR_WRITE_SYSTEM_PREFERENCES,
             OPSTR_CONTROL_AUDIO,
             OPSTR_CONTROL_AUDIO_PARTIAL,
+            OPSTR_EYE_TRACKING_COARSE,
+            OPSTR_EYE_TRACKING_FINE,
+            OPSTR_FACE_TRACKING,
+            OPSTR_HAND_TRACKING,
+            OPSTR_HEAD_TRACKING,
+            OPSTR_SCENE_UNDERSTANDING_COARSE,
+            OPSTR_SCENE_UNDERSTANDING_FINE,
     })
     public @interface AppOpString {}
 
@@ -2579,6 +2642,36 @@
     /** @hide Access to a audio playback and control APIs without capability requirements */
     public static final String OPSTR_CONTROL_AUDIO_PARTIAL = "android:control_audio_partial";
 
+    /** @hide Access coarse eye tracking data. */
+    @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+    public static final String OPSTR_EYE_TRACKING_COARSE = "android:eye_tracking_coarse";
+
+    /** @hide Access fine eye tracking data. */
+    @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+    public static final String OPSTR_EYE_TRACKING_FINE = "android:eye_tracking_fine";
+
+    /** @hide Access face tracking data. */
+    @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+    public static final String OPSTR_FACE_TRACKING = "android:face_tracking";
+
+    /** @hide Access hand tracking data. */
+    @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+    public static final String OPSTR_HAND_TRACKING = "android:hand_tracking";
+
+    /** @hide Access head tracking data. */
+    @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+    public static final String OPSTR_HEAD_TRACKING = "android:head_tracking";
+
+    /** @hide Access coarse scene tracking data. */
+    @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+    public static final String OPSTR_SCENE_UNDERSTANDING_COARSE =
+            "android:scene_understanding_coarse";
+
+    /** @hide Access fine scene tracking data. */
+    @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+    public static final String OPSTR_SCENE_UNDERSTANDING_FINE =
+            "android:scene_understanding_fine";
+
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
     /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2657,6 +2750,14 @@
             Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE,
             Flags.replaceBodySensorPermissionEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE,
             Flags.replaceBodySensorPermissionEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE,
+            // Android XR
+            android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_COARSE : OP_NONE,
+            android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_FINE : OP_NONE,
+            android.xr.Flags.xrManifestEntries() ? OP_FACE_TRACKING : OP_NONE,
+            android.xr.Flags.xrManifestEntries() ? OP_HAND_TRACKING : OP_NONE,
+            android.xr.Flags.xrManifestEntries() ? OP_HEAD_TRACKING : OP_NONE,
+            android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_COARSE : OP_NONE,
+            android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_FINE : OP_NONE,
     };
 
     /**
@@ -3192,6 +3293,41 @@
                 "CONTROL_AUDIO").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(),
         new AppOpInfo.Builder(OP_CONTROL_AUDIO_PARTIAL, OPSTR_CONTROL_AUDIO_PARTIAL,
                 "CONTROL_AUDIO_PARTIAL").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(),
+        new AppOpInfo.Builder(OP_EYE_TRACKING_COARSE, OPSTR_EYE_TRACKING_COARSE,
+                "EYE_TRACKING_COARSE")
+                .setPermission(android.xr.Flags.xrManifestEntries()
+                    ? Manifest.permission.EYE_TRACKING_COARSE : null)
+                .build(),
+        new AppOpInfo.Builder(OP_EYE_TRACKING_FINE, OPSTR_EYE_TRACKING_FINE,
+                "EYE_TRACKING_FINE")
+                .setPermission(android.xr.Flags.xrManifestEntries()
+                    ? Manifest.permission.EYE_TRACKING_FINE : null)
+                .build(),
+        new AppOpInfo.Builder(OP_FACE_TRACKING, OPSTR_FACE_TRACKING,
+                "FACE_TRACKING")
+                .setPermission(android.xr.Flags.xrManifestEntries()
+                    ? Manifest.permission.FACE_TRACKING : null)
+                .build(),
+        new AppOpInfo.Builder(OP_HAND_TRACKING, OPSTR_HAND_TRACKING,
+                "HAND_TRACKING")
+                .setPermission(android.xr.Flags.xrManifestEntries()
+                    ? Manifest.permission.HAND_TRACKING : null)
+                .build(),
+        new AppOpInfo.Builder(OP_HEAD_TRACKING, OPSTR_HEAD_TRACKING,
+                "HEAD_TRACKING")
+                .setPermission(android.xr.Flags.xrManifestEntries()
+                    ? Manifest.permission.HEAD_TRACKING : null)
+                .build(),
+        new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_COARSE, OPSTR_SCENE_UNDERSTANDING_COARSE,
+                "SCENE_UNDERSTANDING_COARSE")
+                .setPermission(android.xr.Flags.xrManifestEntries()
+                    ? Manifest.permission.SCENE_UNDERSTANDING_COARSE : null)
+                .build(),
+        new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_FINE, OPSTR_SCENE_UNDERSTANDING_FINE,
+                "SCENE_UNDERSTANDING_FINE")
+                .setPermission(android.xr.Flags.xrManifestEntries()
+                    ? Manifest.permission.SCENE_UNDERSTANDING_FINE : null)
+                .build(),
     };
 
     // The number of longs needed to form a full bitmask of app ops
@@ -3301,6 +3437,15 @@
     }
 
     /**
+     * Returns whether the provided {@code op} is a valid op code or not.
+     *
+     * @hide
+     */
+    public static boolean isValidOp(int op) {
+        return op >= 0 && op < sAppOpInfos.length;
+    }
+
+    /**
      * @hide
      */
     public static int strDebugOpToOp(String op) {
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index fa977c9..2daa52b 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -228,7 +228,7 @@
     public AutomaticZenRule(Parcel source) {
         enabled = source.readInt() == ENABLED;
         if (source.readInt() == ENABLED) {
-            name = getTrimmedString(source.readString());
+            name = getTrimmedString(source.readString8());
         }
         interruptionFilter = source.readInt();
         conditionId = getTrimmedUri(source.readParcelable(null, android.net.Uri.class));
@@ -238,11 +238,11 @@
                 source.readParcelable(null, android.content.ComponentName.class));
         creationTime = source.readLong();
         mZenPolicy = source.readParcelable(null, ZenPolicy.class);
-        mPkg = source.readString();
+        mPkg = source.readString8();
         mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
         mAllowManualInvocation = source.readBoolean();
         mIconResId = source.readInt();
-        mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
+        mTriggerDescription = getTrimmedString(source.readString8(), MAX_DESC_LENGTH);
         mType = source.readInt();
     }
 
@@ -514,7 +514,7 @@
         dest.writeInt(enabled ? ENABLED : DISABLED);
         if (name != null) {
             dest.writeInt(1);
-            dest.writeString(name);
+            dest.writeString8(name);
         } else {
             dest.writeInt(0);
         }
@@ -524,11 +524,11 @@
         dest.writeParcelable(configurationActivity, 0);
         dest.writeLong(creationTime);
         dest.writeParcelable(mZenPolicy, 0);
-        dest.writeString(mPkg);
+        dest.writeString8(mPkg);
         dest.writeParcelable(mDeviceEffects, 0);
         dest.writeBoolean(mAllowManualInvocation);
         dest.writeInt(mIconResId);
-        dest.writeString(mTriggerDescription);
+        dest.writeString8(mTriggerDescription);
         dest.writeInt(mType);
     }
 
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index eb9feb9..8af5b1b 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -189,6 +189,7 @@
      * @param arguments Any additional arguments that were supplied when the 
      *                  instrumentation was started.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public void onCreate(Bundle arguments) {
     }
 
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 719e438..1b71e73 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -25,6 +25,9 @@
 import static android.app.admin.DevicePolicyResources.UNDEFINED;
 import static android.graphics.drawable.Icon.TYPE_URI;
 import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import static java.util.Objects.requireNonNull;
 
@@ -6001,6 +6004,8 @@
                 contentView.setViewVisibility(p.mTextViewId, View.GONE);
                 contentView.setTextViewText(p.mTextViewId, null);
             }
+
+            updateExpanderAlignment(contentView, p, hasSecondLine);
             setHeaderlessVerticalMargins(contentView, p, hasSecondLine);
 
             // Update margins to leave space for the top line (but not for headerless views like
@@ -6010,12 +6015,29 @@
                 int margin = getContentMarginTop(mContext,
                         R.dimen.notification_2025_content_margin_top);
                 contentView.setViewLayoutMargin(R.id.notification_main_column,
-                        RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX);
+                        RemoteViews.MARGIN_TOP, margin, COMPLEX_UNIT_PX);
             }
 
             return contentView;
         }
 
+        private static void updateExpanderAlignment(RemoteViews contentView,
+                StandardTemplateParams p, boolean hasSecondLine) {
+            if (notificationsRedesignTemplates() && p.mHeaderless) {
+                if (!hasSecondLine) {
+                    // If there's no text, let's center the expand button vertically to align things
+                    // more nicely. This is handled separately for notifications that use a
+                    // NotificationHeaderView, see NotificationHeaderView#centerTopLine.
+                    contentView.setViewLayoutHeight(R.id.expand_button, MATCH_PARENT,
+                            COMPLEX_UNIT_PX);
+                } else {
+                    // Otherwise, just use the default height for the button to keep it top-aligned.
+                    contentView.setViewLayoutHeight(R.id.expand_button, WRAP_CONTENT,
+                            COMPLEX_UNIT_PX);
+                }
+            }
+        }
+
         private static void setHeaderlessVerticalMargins(RemoteViews contentView,
                 StandardTemplateParams p, boolean hasSecondLine) {
             if (Flags.notificationsRedesignTemplates() || !p.mHeaderless) {
@@ -9560,7 +9582,7 @@
                 int marginStart = res.getDimensionPixelSize(
                         R.dimen.notification_2025_content_margin_start);
                 contentView.setViewLayoutMargin(R.id.title,
-                        RemoteViews.MARGIN_START, marginStart, TypedValue.COMPLEX_UNIT_PX);
+                        RemoteViews.MARGIN_START, marginStart, COMPLEX_UNIT_PX);
             }
             if (isLegacyHeaderless) {
                 // Collapsed legacy messaging style has a 1-line limit.
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 726999a..050ef23 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1282,6 +1282,10 @@
      * delegate for (see {@link #canNotifyAsPackage(String)}), or it will not be returned. To query
      * a channel as a notification delegate, call this method from a context created for that
      * package (see {@link Context#createPackageContext(String, int)}).</p>
+     *
+     * <p>If a conversation channel with the given conversation id is not found, this method will
+     * instead return the parent channel with the given channel ID, or {@code null} if neither
+     * exists.</p>
      */
     public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId,
             @NonNull String conversationId) {
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 7a811a1..5b0cf115 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -132,7 +132,7 @@
 per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS
 per-file ComponentOptions.java = file:/services/core/java/com/android/server/wm/OWNERS
-
+per-file Presentation.java = file:/services/core/java/com/android/server/wm/OWNERS
 
 # Multitasking
 per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
diff --git a/core/java/android/app/Presentation.java b/core/java/android/app/Presentation.java
index bdab39d..f39e2dd 100644
--- a/core/java/android/app/Presentation.java
+++ b/core/java/android/app/Presentation.java
@@ -20,6 +20,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
 import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
 
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
+
 import android.annotation.NonNull;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
@@ -34,6 +36,8 @@
 import android.view.Display;
 import android.view.Gravity;
 import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
 import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams.WindowType;
 
@@ -277,6 +281,11 @@
     @Override
     public void show() {
         super.show();
+
+        WindowInsetsController controller = getWindow().getInsetsController();
+        if (controller != null && enablePresentationForConnectedDisplays()) {
+            controller.hide(WindowInsets.Type.systemBars());
+        }
     }
 
     /**
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 67ade79..0085e4f 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -143,3 +143,10 @@
     is_fixed_read_only: true
     bug: "370928384"
 }
+
+flag {
+    name: "device_aware_settings_override"
+    namespace: "virtual_devices"
+    description: "Settings override for virtual devices"
+    bug: "371801645"
+}
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index c4af871..bebca57 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1499,7 +1499,7 @@
     }
 
     @VisibleForTesting
-    static final class DisplayListenerDelegate {
+    public static final class DisplayListenerDelegate {
         public final DisplayListener mListener;
         public volatile long mInternalEventFlagsMask;
 
@@ -1536,7 +1536,7 @@
         }
 
         @VisibleForTesting
-        boolean isEventFilterExplicit() {
+        public boolean isEventFilterExplicit() {
             return mIsEventFilterExplicit;
         }
 
@@ -1892,7 +1892,7 @@
     }
 
     @VisibleForTesting
-    CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() {
+    public CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() {
         return mDisplayListeners;
     }
 }
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 953ee08..5b5360e 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -485,6 +485,9 @@
     /**
      * Returns the list of ContextHubInfo objects describing the available Context Hubs.
      *
+     * To find the list of hubs that include all Hubs (including both Context Hubs and Vendor Hubs),
+     * use the {@link #getHubs()} method instead.
+     *
      * @return the list of ContextHubInfo objects
      *
      * @see ContextHubInfo
@@ -499,8 +502,8 @@
     }
 
     /**
-     * Returns the list of HubInfo objects describing the available hubs (including ContextHub and
-     * VendorHub). This method is primarily used for debugging purposes as most clients care about
+     * Returns the list of HubInfo objects describing the available hubs (including Context Hubs and
+     * Vendor Hubs). This method is primarily used for debugging purposes as most clients care about
      * endpoints and services more than hubs.
      *
      * @return the list of HubInfo objects
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 4cbd5be..1cf43d4 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -2636,7 +2636,7 @@
             enabled = source.readInt() == 1;
             snoozing = source.readInt() == 1;
             if (source.readInt() == 1) {
-                name = source.readString();
+                name = source.readString8();
             }
             zenMode = source.readInt();
             conditionId = source.readParcelable(null, android.net.Uri.class);
@@ -2644,18 +2644,18 @@
             component = source.readParcelable(null, android.content.ComponentName.class);
             configurationActivity = source.readParcelable(null, android.content.ComponentName.class);
             if (source.readInt() == 1) {
-                id = source.readString();
+                id = source.readString8();
             }
             creationTime = source.readLong();
             if (source.readInt() == 1) {
-                enabler = source.readString();
+                enabler = source.readString8();
             }
             zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
             zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
-            pkg = source.readString();
+            pkg = source.readString8();
             allowManualInvocation = source.readBoolean();
-            iconResName = source.readString();
-            triggerDescription = source.readString();
+            iconResName = source.readString8();
+            triggerDescription = source.readString8();
             type = source.readInt();
             userModifiedFields = source.readInt();
             zenPolicyUserModifiedFields = source.readInt();
@@ -2703,7 +2703,7 @@
             dest.writeInt(snoozing ? 1 : 0);
             if (name != null) {
                 dest.writeInt(1);
-                dest.writeString(name);
+                dest.writeString8(name);
             } else {
                 dest.writeInt(0);
             }
@@ -2714,23 +2714,23 @@
             dest.writeParcelable(configurationActivity, 0);
             if (id != null) {
                 dest.writeInt(1);
-                dest.writeString(id);
+                dest.writeString8(id);
             } else {
                 dest.writeInt(0);
             }
             dest.writeLong(creationTime);
             if (enabler != null) {
                 dest.writeInt(1);
-                dest.writeString(enabler);
+                dest.writeString8(enabler);
             } else {
                 dest.writeInt(0);
             }
             dest.writeParcelable(zenPolicy, 0);
             dest.writeParcelable(zenDeviceEffects, 0);
-            dest.writeString(pkg);
+            dest.writeString8(pkg);
             dest.writeBoolean(allowManualInvocation);
-            dest.writeString(iconResName);
-            dest.writeString(triggerDescription);
+            dest.writeString8(iconResName);
+            dest.writeString8(triggerDescription);
             dest.writeInt(type);
             dest.writeInt(userModifiedFields);
             dest.writeInt(zenPolicyUserModifiedFields);
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index f58baff..4fc894c 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -789,6 +789,12 @@
             in @nullable ImeTracker.Token statsToken);
 
     /**
+     * Updates the currently animating insets types of a remote process.
+     */
+    @EnforcePermission("MANAGE_APP_TOKENS")
+    void updateDisplayWindowAnimatingTypes(int displayId, int animatingTypes);
+
+    /**
      * Called to get the expected window insets.
      *
      * @return {@code true} if system bars are always consumed.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 1f8f0820..7d6d5a2 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -272,6 +272,15 @@
             in @nullable ImeTracker.Token imeStatsToken);
 
     /**
+     * Notifies WindowState what insets types are currently running within the Window.
+     * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning).
+     *
+     * @param window The window that is insets animaiton is running.
+     * @param animatingTypes Indicates the currently animating insets types.
+     */
+    oneway void updateAnimatingTypes(IWindow window, int animatingTypes);
+
+    /**
      * Called when the system gesture exclusion has changed.
      */
     oneway void reportSystemGestureExclusionChanged(IWindow window, in List<Rect> exclusionRects);
@@ -372,14 +381,4 @@
      */
     oneway void notifyImeWindowVisibilityChangedFromClient(IWindow window, boolean visible,
             in ImeTracker.Token statsToken);
-
-    /**
-     * Notifies WindowState whether inset animations are currently running within the Window.
-     * This value is used by the server to vote for refresh rate.
-     * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning).
-     *
-     * @param window The window that is insets animaiton is running.
-     * @param running Indicates the insets animation state.
-     */
-    oneway void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running);
 }
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 0d82acd..462c5c6 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -211,12 +211,12 @@
         }
 
         /**
-         * Notifies when the state of running animation is changed. The state is either "running" or
-         * "idle".
+         * Notifies when the insets types of running animation have changed. The animatingTypes
+         * contain all types, which have an ongoing animation.
          *
-         * @param running {@code true} if there is any animation running; {@code false} otherwise.
+         * @param animatingTypes the {@link InsetsType}s that are currently animating
          */
-        default void notifyAnimationRunningStateChanged(boolean running) {}
+        default void updateAnimatingTypes(@InsetsType int animatingTypes) {}
 
         /** @see ViewRootImpl#isHandlingPointerEvent */
         default boolean isHandlingPointerEvent() {
@@ -665,6 +665,9 @@
     /** Set of inset types which are requested visible which are reported to the host */
     private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
 
+    /** Set of insets types which are currently animating */
+    private @InsetsType int mAnimatingTypes = 0;
+
     /** Set of inset types that we have controls of */
     private @InsetsType int mControllableTypes;
 
@@ -745,9 +748,10 @@
                             mFrame, mFromState, mToState, RESIZE_INTERPOLATOR,
                             ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this);
                     if (mRunningAnimations.isEmpty()) {
-                        mHost.notifyAnimationRunningStateChanged(true);
+                        mHost.updateAnimatingTypes(runner.getTypes());
                     }
                     mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
+                    mAnimatingTypes |= runner.getTypes();
                 }
             };
 
@@ -1564,9 +1568,8 @@
             }
         }
         ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
-        if (mRunningAnimations.isEmpty()) {
-            mHost.notifyAnimationRunningStateChanged(true);
-        }
+        mAnimatingTypes |= runner.getTypes();
+        mHost.updateAnimatingTypes(mAnimatingTypes);
         mRunningAnimations.add(new RunningAnimation(runner, animationType));
         if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
                 + useInsetsAnimationThread);
@@ -1827,7 +1830,7 @@
                     dispatchAnimationEnd(runningAnimation.runner.getAnimation());
                 } else {
                     if (Flags.refactorInsetsController()) {
-                        if (removedTypes == ime()
+                        if ((removedTypes & ime()) != 0
                                 && control.getAnimationType() == ANIMATION_TYPE_HIDE) {
                             if (mHost != null) {
                                 // if the (hide) animation is cancelled, the
@@ -1842,9 +1845,11 @@
                 break;
             }
         }
-        if (mRunningAnimations.isEmpty()) {
-            mHost.notifyAnimationRunningStateChanged(false);
+        if (removedTypes > 0) {
+            mAnimatingTypes &= ~removedTypes;
+            mHost.updateAnimatingTypes(mAnimatingTypes);
         }
+
         onAnimationStateChanged(removedTypes, false /* running */);
     }
 
@@ -1969,14 +1974,6 @@
         return animatingTypes;
     }
 
-    private @InsetsType int computeAnimatingTypes() {
-        int animatingTypes = 0;
-        for (int i = 0; i < mRunningAnimations.size(); i++) {
-            animatingTypes |= mRunningAnimations.get(i).runner.getTypes();
-        }
-        return animatingTypes;
-    }
-
     /**
      * Called when finishing setting requested visible types or finishing setting controls.
      *
@@ -1989,7 +1986,7 @@
             // report its requested visibility at the end of the animation, otherwise we would
             // lose the leash, and it would disappear during the animation
             // TODO(b/326377046) revisit this part and see if we can make it more general
-            typesToReport = mRequestedVisibleTypes | (computeAnimatingTypes() & ime());
+            typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime());
         } else {
             typesToReport = mRequestedVisibleTypes;
         }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9498407..7fd7be8 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2540,11 +2540,12 @@
     }
 
     /**
-     * Notify the when the running state of a insets animation changed.
+     * Notify the when the animating insets types have changed.
      */
     @VisibleForTesting
-    public void notifyInsetsAnimationRunningStateChanged(boolean running) {
+    public void updateAnimatingTypes(@InsetsType int animatingTypes) {
         if (sToolkitSetFrameRateReadOnlyFlagValue) {
+            boolean running = animatingTypes != 0;
             if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                 Trace.instant(Trace.TRACE_TAG_VIEW,
                         TextUtils.formatSimple("notifyInsetsAnimationRunningStateChanged(%s)",
@@ -2552,7 +2553,7 @@
             }
             mInsetsAnimationRunning = running;
             try {
-                mWindowSession.notifyInsetsAnimationRunningStateChanged(mWindow, running);
+                mWindowSession.updateAnimatingTypes(mWindow, animatingTypes);
             } catch (RemoteException e) {
             }
         }
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index 889acca4..8954df6 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -171,6 +171,13 @@
     }
 
     @Override
+    public void updateAnimatingTypes(@WindowInsets.Type.InsetsType int animatingTypes) {
+        if (mViewRoot != null) {
+            mViewRoot.updateAnimatingTypes(animatingTypes);
+        }
+    }
+
+    @Override
     public boolean hasAnimationCallbacks() {
         if (mViewRoot.mView == null) {
             return false;
@@ -275,13 +282,6 @@
     }
 
     @Override
-    public void notifyAnimationRunningStateChanged(boolean running) {
-        if (mViewRoot != null) {
-            mViewRoot.notifyInsetsAnimationRunningStateChanged(running);
-        }
-    }
-
-    @Override
     public boolean isHandlingPointerEvent() {
         return mViewRoot != null && mViewRoot.isHandlingPointerEvent();
     }
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 24647f4..196ae5e 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -625,6 +625,12 @@
     int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000
 
     /**
+     * Transition flag: Indicates that aod is showing hidden by entering doze
+     * @hide
+     */
+    int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000
+
+    /**
      * @hide
      */
     @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
@@ -643,6 +649,7 @@
             TRANSIT_FLAG_KEYGUARD_OCCLUDING,
             TRANSIT_FLAG_KEYGUARD_UNOCCLUDING,
             TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH,
+            TRANSIT_FLAG_AOD_APPEARING,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface TransitionFlags {}
@@ -659,7 +666,8 @@
             (TRANSIT_FLAG_KEYGUARD_GOING_AWAY
             | TRANSIT_FLAG_KEYGUARD_APPEARING
             | TRANSIT_FLAG_KEYGUARD_OCCLUDING
-            | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING);
+            | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING
+            | TRANSIT_FLAG_AOD_APPEARING);
 
     /**
      * Remove content mode: Indicates remove content mode is currently not defined.
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 72a595d..0a86ff8 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -597,6 +597,11 @@
     }
 
     @Override
+    public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) {
+        // NO-OP
+    }
+
+    @Override
     public void reportSystemGestureExclusionChanged(android.view.IWindow window,
             List<Rect> exclusionRects) {
     }
@@ -679,11 +684,6 @@
             @NonNull ImeTracker.Token statsToken) {
     }
 
-    @Override
-    public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) {
-        // NO-OP
-    }
-
     void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) {
         IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder();
         IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder();
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 49a11ca..80a9cbc 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -235,6 +235,14 @@
 }
 
 flag {
+    name: "request_rectangle_with_source"
+    namespace: "accessibility"
+    description: "Request rectangle on screen with source parameter"
+    bug: "391877896"
+    is_exported: true
+}
+
+flag {
     name: "restore_a11y_secure_settings_on_hsum_device"
     namespace: "accessibility"
     description: "Grab the a11y settings and send the settings restored broadcast for current visible foreground user"
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index aa0111a..60178cd 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -225,6 +225,7 @@
             PHASE_SERVER_UPDATE_CLIENT_VISIBILITY,
             PHASE_WM_DISPLAY_IME_CONTROLLER_SET_IME_REQUESTED_VISIBLE,
             PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES,
+            PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface Phase {}
@@ -445,6 +446,9 @@
     /** The control target reported its requestedVisibleTypes back to WindowManagerService. */
     int PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES =
             ImeProtoEnums.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES;
+    /** The requestedVisibleTypes have not been changed, so this request is not continued. */
+    int PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED =
+            ImeProtoEnums.PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED;
 
     /**
      * Called when an IME request is started.
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index 16f4114..c81c2bb 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -196,3 +196,10 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "report_animating_insets_types"
+  namespace: "input_method"
+  description: "Adding animating insets types and report IME visibility at the beginning of hiding"
+  bug: "393049691"
+}
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 118edc2..fa7b74f 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -242,7 +242,7 @@
 
         @Override
         public void onNullBinding(ComponentName name) {
-            enqueueDeferredUnbindServiceMessage();
+            unbindNow();
         }
 
         @Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 99fe0cb..5e828ba 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5211,7 +5211,11 @@
      */
     @Nullable
     public String getFontVariationSettings() {
-        return mTextPaint.getFontVariationSettings();
+        if (Flags.typefaceRedesignReadonly()) {
+            return mTextPaint.getFontVariationOverride();
+        } else {
+            return mTextPaint.getFontVariationSettings();
+        }
     }
 
     /**
@@ -5567,10 +5571,10 @@
                             Math.clamp(400 + mFontWeightAdjustment,
                                     FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
                 }
-                mTextPaint.setFontVariationSettings(
+                mTextPaint.setFontVariationOverride(
                         FontVariationAxis.toFontVariationSettings(axes));
             } else {
-                mTextPaint.setFontVariationSettings(fontVariationSettings);
+                mTextPaint.setFontVariationOverride(fontVariationSettings);
             }
             effective = true;
         } else {
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 4aeedbb..42bf6d1 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -97,6 +97,8 @@
     ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
             true),
     ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false),
+    ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX(
+            Flags::enableDragToDesktopIncomingTransitionsBugfix, false),
     ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
     ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
     ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 2e36e9a..684f320 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -811,4 +811,13 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
-}
\ No newline at end of file
+}
+flag {
+    name: "enable_drag_to_desktop_incoming_transitions_bugfix"
+    namespace: "lse_desktop_experience"
+    description: "Enables bugfix handling incoming transitions during the DragToDesktop transition."
+    bug: "397135730"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java
index ad73294..4663d62 100644
--- a/core/java/com/android/internal/content/PackageMonitor.java
+++ b/core/java/com/android/internal/content/PackageMonitor.java
@@ -36,6 +36,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.WeaklyReferencedCallback;
 import com.android.internal.os.BackgroundThread;
 
 import java.lang.ref.WeakReference;
@@ -46,6 +47,7 @@
  * Helper class for monitoring the state of packages: adding, removing,
  * updating, and disappearing and reappearing on the SD card.
  */
+@WeaklyReferencedCallback
 public abstract class PackageMonitor extends android.content.BroadcastReceiver {
     static final String TAG = "PackageMonitor";
 
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index b19967a..0e6eb18 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -80,7 +80,11 @@
                     (Process.myUid() == Process.SYSTEM_UID) ? DeviceProtos.parsedFlagsProtoPaths()
                             : Arrays.asList(DeviceProtos.PATHS);
             for (String fileName : defaultFlagProtoFiles) {
-                try (var inputStream = new FileInputStream(fileName)) {
+                final File protoFile = new File(fileName);
+                if (!protoFile.isFile() || !protoFile.canRead()) {
+                    continue;
+                }
+                try (var inputStream = new FileInputStream(protoFile)) {
                     loadAconfigDefaultValues(inputStream.readAllBytes());
                 } catch (IOException e) {
                     Slog.w(LOG_TAG, "Failed to read Aconfig values from " + fileName, e);
@@ -120,6 +124,9 @@
 
         final var settingsFile = new File(Environment.getUserSystemDirectory(0),
                 "settings_config.xml");
+        if (!settingsFile.isFile() || !settingsFile.canRead()) {
+            return;
+        }
         try (var inputStream = new FileInputStream(settingsFile)) {
             TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
             if (parser.next() != XmlPullParser.END_TAG && "settings".equals(parser.getName())) {
@@ -186,7 +193,7 @@
                 }
             }
         } catch (IOException | XmlPullParserException e) {
-            Slog.e(LOG_TAG, "Failed to read Aconfig values from settings_config.xml", e);
+            Slog.w(LOG_TAG, "Failed to read Aconfig values from settings_config.xml", e);
         }
     }
 
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
index 69c0480..7ee22f3 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
@@ -157,7 +157,7 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        sForInternedString.parcel(this.name, dest, flags);
+        dest.writeString(this.name);
         dest.writeInt(this.getIcon());
         dest.writeInt(this.getLabelRes());
         dest.writeCharSequence(this.getNonLocalizedLabel());
@@ -175,7 +175,7 @@
         // We use the boot classloader for all classes that we load.
         final ClassLoader boot = Object.class.getClassLoader();
         //noinspection ConstantConditions
-        this.name = sForInternedString.unparcel(in);
+        this.name = in.readString();
         this.icon = in.readInt();
         this.labelRes = in.readInt();
         this.nonLocalizedLabel = in.readCharSequence();
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 7018ebc..5a180d7 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -82,7 +82,7 @@
      * Notify system UI the immersive mode changed. This shall be removed when client immersive is
      * enabled.
      */
-    void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+    void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode, int windowType);
 
     void dismissKeyboardShortcutsMenu();
     void toggleKeyboardShortcutsMenu(int deviceId);
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 9acb242..a1961ae 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -268,6 +268,9 @@
      72dp (content margin) - 12dp (action padding) - 4dp (button inset) -->
     <dimen name="notification_2025_actions_margin_start">56dp</dimen>
 
+    <!-- Notification action button text size -->
+    <dimen name="notification_2025_action_text_size">16sp</dimen>
+
     <!-- The margin on the end of most content views (ignores the expander) -->
     <dimen name="notification_content_margin_end">16dp</dimen>
 
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index 8ef105f..de5f0ff 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -177,8 +177,10 @@
     @RequiresFlagsEnabled(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED)
     public void test_refreshRateRegistration_implicitRRCallbacksEnabled()
             throws RemoteException {
+        DisplayManager.DisplayListener displayListener1 =
+                Mockito.mock(DisplayManager.DisplayListener.class);
         // Subscription without supplied events doesn't subscribe to RR events
-        mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+        mDisplayManagerGlobal.registerDisplayListener(displayListener1, mHandler,
                 ALL_DISPLAY_EVENTS, /* packageName= */ null,
                 /* isEventFilterExplicit */ false);
         Mockito.verify(mDisplayManager)
@@ -187,7 +189,9 @@
         // After registering to refresh rate changes, subscription without supplied events subscribe
         // to RR events
         mDisplayManagerGlobal.registerForRefreshRateChanges();
-        mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+        DisplayManager.DisplayListener displayListener2 =
+                Mockito.mock(DisplayManager.DisplayListener.class);
+        mDisplayManagerGlobal.registerDisplayListener(displayListener2, mHandler,
                 ALL_DISPLAY_EVENTS, /* packageName= */ null,
                 /* isEventFilterExplicit */ false);
         Mockito.verify(mDisplayManager)
@@ -203,7 +207,9 @@
         }
 
         // Subscription to RR when events are supplied doesn't happen
-        mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+        DisplayManager.DisplayListener displayListener3 =
+                Mockito.mock(DisplayManager.DisplayListener.class);
+        mDisplayManagerGlobal.registerDisplayListener(displayListener3, mHandler,
                 ALL_DISPLAY_EVENTS, /* packageName= */ null,
                 /* isEventFilterExplicit */ true);
         Mockito.verify(mDisplayManager)
@@ -214,7 +220,6 @@
         int subscribedListenersCount = 0;
         int nonSubscribedListenersCount = 0;
         for (DisplayManagerGlobal.DisplayListenerDelegate delegate: delegates) {
-
             if (delegate.isEventFilterExplicit()) {
                 assertEquals(ALL_DISPLAY_EVENTS, delegate.mInternalEventFlagsMask);
                 nonSubscribedListenersCount++;
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 4516e9c..af87af0 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -1195,6 +1195,23 @@
         });
     }
 
+    @Test
+    public void testAnimatingTypes() throws Exception {
+        prepareControls();
+
+        final int types = navigationBars() | statusBars();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            clearInvocations(mTestHost);
+            mController.hide(types);
+            // quickly jump to final state by cancelling it.
+            mController.cancelExistingAnimations();
+        });
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(mTestHost, times(1)).updateAnimatingTypes(eq(types));
+        verify(mTestHost, times(1)).updateAnimatingTypes(eq(0) /* animatingTypes */);
+    }
+
     private void waitUntilNextFrame() throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT,
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index c40137f..f5d1e7a 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1054,7 +1054,7 @@
         ViewRootImpl viewRootImpl = mView.getViewRootImpl();
         sInstrumentation.runOnMainSync(() -> {
             mView.invalidate();
-            viewRootImpl.notifyInsetsAnimationRunningStateChanged(true);
+            viewRootImpl.updateAnimatingTypes(Type.systemBars());
             mView.invalidate();
         });
         sInstrumentation.waitForIdleSync();
diff --git a/graphics/java/android/graphics/pdf/OWNERS b/graphics/java/android/graphics/pdf/OWNERS
index 057dc0d..24557b4 100644
--- a/graphics/java/android/graphics/pdf/OWNERS
+++ b/graphics/java/android/graphics/pdf/OWNERS
@@ -4,4 +4,3 @@
 djsollen@google.com
 sumir@google.com
 svetoslavganov@android.com
-svetoslavganov@google.com
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 3aefcd5..9087da3 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -552,7 +552,9 @@
 
     private fun createAppBubble(usePendingIntent: Boolean = false): Bubble {
         val target = Intent(context, TestActivity::class.java)
+        val component = ComponentName(context, TestActivity::class.java)
         target.setPackage(context.packageName)
+        target.setComponent(component)
         if (usePendingIntent) {
             // Robolectric doesn't seem to play nice with PendingIntents, have to mock it.
             val pendingIntent = mock<PendingIntent>()
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
index 7b583137..14c152102 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
@@ -19,7 +19,9 @@
 import android.animation.AnimatorTestRule
 import android.content.Context
 import android.content.pm.LauncherApps
+import android.graphics.Insets
 import android.graphics.PointF
+import android.graphics.Rect
 import android.os.Handler
 import android.os.UserManager
 import android.view.IWindowManager
@@ -61,6 +63,7 @@
 import com.android.wm.shell.shared.TransactionPool
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.bubbles.DeviceConfig
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
@@ -80,6 +83,10 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class BubbleBarLayerViewTest {
+    companion object {
+        const val SCREEN_WIDTH = 2000
+        const val SCREEN_HEIGHT = 1000
+    }
 
     @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
 
@@ -111,6 +118,16 @@
 
         bubblePositioner = BubblePositioner(context, windowManager)
         bubblePositioner.setShowingInBubbleBar(true)
+        val deviceConfig =
+            DeviceConfig(
+                windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT),
+                isLargeScreen = true,
+                isSmallTablet = false,
+                isLandscape = true,
+                isRtl = false,
+                insets = Insets.of(10, 20, 30, 40)
+            )
+        bubblePositioner.update(deviceConfig)
 
         testBubblesList = mutableListOf()
         val bubbleData = mock<BubbleData>()
@@ -313,6 +330,48 @@
         assertThat(uiEventLoggerFake.logs[0]).hasBubbleInfo(bubble)
     }
 
+    @Test
+    fun testUpdateExpandedView_updateLocation() {
+        bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+        val bubble = createBubble("first")
+
+        getInstrumentation().runOnMainSync {
+            bubbleBarLayerView.showExpandedView(bubble)
+        }
+        waitForExpandedViewAnimation()
+
+        val previousX = bubble.bubbleBarExpandedView!!.x
+
+        bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT
+        getInstrumentation().runOnMainSync {
+            bubbleBarLayerView.updateExpandedView()
+        }
+
+        assertThat(bubble.bubbleBarExpandedView!!.x).isNotEqualTo(previousX)
+    }
+
+    @Test
+    fun testUpdatedExpandedView_updateLocation_skipWhileAnimating() {
+        bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+        val bubble = createBubble("first")
+
+        getInstrumentation().runOnMainSync {
+            bubbleBarLayerView.showExpandedView(bubble)
+        }
+        waitForExpandedViewAnimation()
+
+        val previousX = bubble.bubbleBarExpandedView!!.x
+        bubble.bubbleBarExpandedView!!.isAnimating = true
+
+        bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT
+        getInstrumentation().runOnMainSync {
+            bubbleBarLayerView.updateExpandedView()
+        }
+
+        // Expanded view is not updated while animating
+        assertThat(bubble.bubbleBarExpandedView!!.x).isEqualTo(previousX)
+    }
+
     private fun createBubble(key: String): Bubble {
         val bubbleTaskView = FakeBubbleTaskViewFactory(context, mainExecutor).create()
         val bubbleBarExpandedView =
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index d50a14c..c2aa146 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -79,7 +79,7 @@
             android:layout_marginEnd="4dp">
 
             <Button
-                android:layout_width="94dp"
+                android:layout_width="108dp"
                 android:layout_height="60dp"
                 android:id="@+id/maximize_menu_size_toggle_button"
                 style="?android:attr/buttonBarButtonStyle"
@@ -126,7 +126,7 @@
                 <Button
                     android:id="@+id/maximize_menu_snap_left_button"
                     style="?android:attr/buttonBarButtonStyle"
-                    android:layout_width="41dp"
+                    android:layout_width="48dp"
                     android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
                     android:layout_marginEnd="4dp"
                     android:background="@drawable/desktop_mode_maximize_menu_button_background"
@@ -137,7 +137,7 @@
                 <Button
                     android:id="@+id/maximize_menu_snap_right_button"
                     style="?android:attr/buttonBarButtonStyle"
-                    android:layout_width="41dp"
+                    android:layout_width="48dp"
                     android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
                     android:background="@drawable/desktop_mode_maximize_menu_button_background"
                     android:importantForAccessibility="yes"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index a0c68ad..32660e8 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -618,6 +618,15 @@
     <!-- The vertical inset to apply to the app chip's ripple drawable -->
     <dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen>
 
+     <!-- The corner radius of the windowing actions pill buttons's ripple drawable -->
+     <dimen name="desktop_mode_handle_menu_windowing_action_ripple_radius">24dp</dimen>
+     <!-- The horizontal/vertical inset to apply to the ripple drawable effect of windowing
+     actions pill central buttons -->
+     <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_base">2dp</dimen>
+     <!-- The horizontal/vertical vertical inset to apply to the ripple drawable effect of windowing
+     actions pill edge buttons -->
+     <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_shift">4dp</dimen>
+
     <!-- The corner radius of the minimize button's ripple drawable -->
     <dimen name="desktop_mode_header_minimize_ripple_radius">18dp</dimen>
     <!-- The vertical inset to apply to the minimize button's ripple drawable -->
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 00c446c..7acad50 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
@@ -374,7 +374,7 @@
      * of the display's root [TaskDisplayArea] is set to WINDOWING_MODE_FREEFORM.
      */
     public static boolean enterDesktopByDefaultOnFreeformDisplay(@NonNull Context context) {
-        if (!Flags.enterDesktopByDefaultOnFreeformDisplays()) {
+        if (!DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue()) {
             return false;
         }
         return SystemProperties.getBoolean(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP,
@@ -387,7 +387,7 @@
      * screen.
      */
     public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) {
-        if (!Flags.enableDragToMaximize()) {
+        if (!DesktopExperienceFlags.ENABLE_DRAG_TO_MAXIMIZE.isTrue()) {
             return false;
         }
         return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 5355138..26c3626 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -147,10 +147,9 @@
     /** To be overridden by subclasses to adjust the animation surface change. */
     void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
         // Update the surface position and alpha.
-        if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
-                && mAnimation.getExtensionEdges() != 0x0
+        if (mAnimation.getExtensionEdges() != 0x0
                 && !(mChange.hasFlags(FLAG_TRANSLUCENT)
-                        && mChange.getActivityComponent() != null)) {
+                && mChange.getActivityComponent() != null)) {
             // Extend non-translucent activities
             t.setEdgeExtensionEffect(mLeash, mAnimation.getExtensionEdges());
         }
@@ -189,8 +188,7 @@
     @CallSuper
     void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
         onAnimationUpdate(t, mAnimation.getDuration());
-        if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
-                && mAnimation.getExtensionEdges() != 0x0) {
+        if (mAnimation.getExtensionEdges() != 0x0) {
             t.setEdgeExtensionEffect(mLeash, /* edge */ 0);
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index c3e783d..85b7ac2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -20,11 +20,9 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
 import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
-import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
 
@@ -143,10 +141,6 @@
             // ending states.
             prepareForJumpCut(info, startTransaction);
         } else {
-            if (!com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) {
-                addEdgeExtensionIfNeeded(startTransaction, finishTransaction,
-                        postStartTransactionCallbacks, adapters);
-            }
             addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
             for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
                 duration = Math.max(duration, adapter.getDurationHint());
@@ -329,34 +323,6 @@
         }
     }
 
-    /** Adds edge extension to the surfaces that have such an animation property. */
-    private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks,
-            @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
-        for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
-            final Animation animation = adapter.mAnimation;
-            if (animation.getExtensionEdges() == 0) {
-                continue;
-            }
-            if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT)
-                    && adapter.mChange.getActivityComponent() != null) {
-                // Skip edge extension for translucent activity.
-                continue;
-            }
-            final TransitionInfo.Change change = adapter.mChange;
-            if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) {
-                // Need to screenshot after startTransaction is applied otherwise activity
-                // may not be visible or ready yet.
-                postStartTransactionCallbacks.add(
-                        t -> edgeExtendWindow(change, animation, t, finishTransaction));
-            } else {
-                // Can screenshot now (before startTransaction is applied)
-                edgeExtendWindow(change, animation, startTransaction, finishTransaction);
-            }
-        }
-    }
-
     /** Adds background color to the transition if any animation has such a property. */
     private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 313d151..d948928 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -364,7 +364,7 @@
             @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
         return new Bubble(intent,
                 user,
-                /* key= */ getAppBubbleKeyForApp(intent.getIntent().getPackage(), user),
+                /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
                 mainExecutor, bgExecutor);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 29837dc..677c21c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -473,7 +473,7 @@
 
     /** Updates the expanded view size and position. */
     public void updateExpandedView() {
-        if (mExpandedView == null || mExpandedBubble == null) return;
+        if (mExpandedView == null || mExpandedBubble == null || mExpandedView.isAnimating()) return;
         boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
         mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(),
                 isOverflowExpanded, mTempRect);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index df82091..dd2050a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -461,6 +461,14 @@
             }
         }
 
+        private void setAnimating(boolean imeAnimationOngoing) {
+            int animatingTypes = imeAnimationOngoing ? WindowInsets.Type.ime() : 0;
+            try {
+                mWmService.updateDisplayWindowAnimatingTypes(mDisplayId, animatingTypes);
+            } catch (RemoteException e) {
+            }
+        }
+
         private int imeTop(float surfaceOffset, float surfacePositionY) {
             // surfaceOffset is already offset by the surface's top inset, so we need to subtract
             // the top inset so that the return value is in screen coordinates.
@@ -619,6 +627,9 @@
                                 + imeTop(hiddenY, defaultY) + "->" + imeTop(shownY, defaultY)
                                 + " showing:" + (mAnimationDirection == DIRECTION_SHOW));
                     }
+                    if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+                        setAnimating(true);
+                    }
                     int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY),
                             imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW,
                             isFloating, t);
@@ -666,6 +677,8 @@
                     }
                     if (!android.view.inputmethod.Flags.refactorInsetsController()) {
                         dispatchEndPositioning(mDisplayId, mCancelled, t);
+                    } else if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+                        setAnimating(false);
                     }
                     if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
                         ImeTracker.forLogging().onProgress(mStatsToken,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 59acdc5..48fadc0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -100,6 +100,7 @@
 import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
 import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
+import com.android.wm.shell.desktopmode.DragToDisplayTransitionHandler;
 import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.OverviewToDesktopTransitionObserver;
@@ -770,7 +771,8 @@
             DesksOrganizer desksOrganizer,
             DesksTransitionObserver desksTransitionObserver,
             UserProfileContexts userProfileContexts,
-            DesktopModeCompatPolicy desktopModeCompatPolicy) {
+            DesktopModeCompatPolicy desktopModeCompatPolicy,
+            DragToDisplayTransitionHandler dragToDisplayTransitionHandler) {
         return new DesktopTasksController(
                 context,
                 shellInit,
@@ -808,7 +810,8 @@
                 desksOrganizer,
                 desksTransitionObserver,
                 userProfileContexts,
-                desktopModeCompatPolicy);
+                desktopModeCompatPolicy,
+                dragToDisplayTransitionHandler);
     }
 
     @WMSingleton
@@ -934,6 +937,12 @@
 
     @WMSingleton
     @Provides
+    static DragToDisplayTransitionHandler provideDragToDisplayTransitionHandler() {
+        return new DragToDisplayTransitionHandler();
+    }
+
+    @WMSingleton
+    @Provides
     static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler(
             Context context,
             Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
index c9a63ff..e89aafe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -27,9 +27,9 @@
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.IWindowManager
 import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.DesktopExperienceFlags
 import android.window.WindowContainerTransaction
 import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
@@ -47,31 +47,9 @@
 ) {
 
     fun refreshDisplayWindowingMode() {
-        if (!Flags.enableDisplayWindowingModeSwitching()) return
-        // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
-        val isExtendedDisplayEnabled =
-            0 !=
-                Settings.Global.getInt(
-                    context.contentResolver,
-                    DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
-                    0,
-                )
-        if (!isExtendedDisplayEnabled) {
-            // No action needed in mirror or projected mode.
-            return
-        }
+        if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
 
-        val hasNonDefaultDisplay =
-            rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId ->
-                displayId != DEFAULT_DISPLAY
-            }
-        val targetDisplayWindowingMode =
-            if (hasNonDefaultDisplay) {
-                WINDOWING_MODE_FREEFORM
-            } else {
-                // Use the default display windowing mode when no non-default display.
-                windowManager.getWindowingMode(DEFAULT_DISPLAY)
-            }
+        val targetDisplayWindowingMode = getTargetWindowingModeForDefaultDisplay()
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
         requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
         val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
@@ -111,6 +89,25 @@
         transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
     }
 
+    private fun getTargetWindowingModeForDefaultDisplay(): Int {
+        if (isExtendedDisplayEnabled() && hasExternalDisplay()) {
+            return WINDOWING_MODE_FREEFORM
+        }
+        return windowManager.getWindowingMode(DEFAULT_DISPLAY)
+    }
+
+    // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
+    private fun isExtendedDisplayEnabled() =
+        0 !=
+            Settings.Global.getInt(
+                context.contentResolver,
+                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+                0,
+            )
+
+    private fun hasExternalDisplay() =
+        rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
+
     private fun logV(msg: String, vararg arguments: Any?) {
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 04e609e..03423ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -1007,6 +1007,21 @@
     fun saveBoundsBeforeFullImmersive(taskId: Int, bounds: Rect) =
         boundsBeforeFullImmersiveByTaskId.set(taskId, Rect(bounds))
 
+    /** Returns the current state of the desktop, formatted for usage by remote clients. */
+    fun getDeskDisplayStateForRemote(): Array<DisplayDeskState> =
+        desktopData
+            .desksSequence()
+            .groupBy { it.displayId }
+            .map { (displayId, desks) ->
+                val activeDeskId = desktopData.getActiveDesk(displayId)?.deskId
+                DisplayDeskState().apply {
+                    this.displayId = displayId
+                    this.activeDeskId = activeDeskId ?: INVALID_DESK_ID
+                    this.deskIds = desks.map { it.deskId }.toIntArray()
+                }
+            }
+            .toTypedArray()
+
     /** TODO: b/389960283 - consider updating only the changing desks. */
     private fun updatePersistentRepository(displayId: Int) {
         val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 2d9aea0..fca5084 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -205,6 +205,7 @@
     private val desksTransitionObserver: DesksTransitionObserver,
     private val userProfileContexts: UserProfileContexts,
     private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
+    private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
 ) :
     RemoteCallable<DesktopTasksController>,
     Transitions.TransitionHandler,
@@ -811,7 +812,7 @@
             willExitDesktop(
                 triggerTaskId = taskInfo.taskId,
                 displayId = displayId,
-                forceToFullscreen = false,
+                forceExitDesktop = false,
             )
         taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
         val desktopExitRunnable =
@@ -884,7 +885,7 @@
 
         snapEventHandler.removeTaskIfTiled(displayId, taskId)
         taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
-        val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false)
+        val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
         val desktopExitRunnable =
             performDesktopExitCleanUp(
                 wct = wct,
@@ -977,7 +978,7 @@
     ) {
         logV("moveToFullscreenWithAnimation taskId=%d", task.taskId)
         val wct = WindowContainerTransaction()
-        val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true)
+        val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceExitDesktop = true)
         val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop)
 
         // We are moving a freeform task to fullscreen, put the home task under the fullscreen task.
@@ -996,7 +997,14 @@
         deactivationRunnable?.invoke(transition)
 
         // handles case where we are moving to full screen without closing all DW tasks.
-        if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) {
+        if (
+            !taskRepository.isOnlyVisibleNonClosingTask(task.taskId)
+            // This callback is already invoked by |addMoveToFullscreenChanges| when one of these
+            // flags is enabled.
+            &&
+                !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
+                !Flags.enableDesktopWindowingPip()
+        ) {
             desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
                 FULLSCREEN_ANIMATION_DURATION
             )
@@ -1893,16 +1901,24 @@
     private fun willExitDesktop(
         triggerTaskId: Int,
         displayId: Int,
-        forceToFullscreen: Boolean,
+        forceExitDesktop: Boolean,
     ): Boolean {
+        if (
+            forceExitDesktop &&
+                (Flags.enableDesktopWindowingPip() ||
+                    DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue)
+        ) {
+            // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when
+            // explicitly going fullscreen, so there's no point in checking the desktop state.
+            return true
+        }
         if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
             if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) {
                 return false
             }
         } else if (
             Flags.enableDesktopWindowingPip() &&
-                taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
-                !forceToFullscreen
+                taskRepository.isMinimizedPipPresentInDisplay(displayId)
         ) {
             return false
         } else {
@@ -2295,7 +2311,7 @@
                 willExitDesktop(
                     triggerTaskId = task.taskId,
                     displayId = task.displayId,
-                    forceToFullscreen = true,
+                    forceExitDesktop = true,
                 ),
         )
         wct.reorder(task.token, true)
@@ -2328,7 +2344,7 @@
                         willExitDesktop(
                             triggerTaskId = task.taskId,
                             displayId = task.displayId,
-                            forceToFullscreen = true,
+                            forceExitDesktop = true,
                         ),
                 )
                 return wct
@@ -2433,7 +2449,7 @@
                         willExitDesktop(
                             triggerTaskId = task.taskId,
                             displayId = task.displayId,
-                            forceToFullscreen = true,
+                            forceExitDesktop = true,
                         ),
                 )
             }
@@ -2471,7 +2487,7 @@
                     willExitDesktop(
                         triggerTaskId = task.taskId,
                         displayId = task.displayId,
-                        forceToFullscreen = true,
+                        forceExitDesktop = true,
                     ),
             )
         }
@@ -2701,10 +2717,12 @@
     /**
      * Adds split screen changes to a transaction. Note that bounds are not reset here due to
      * animation; see {@link onDesktopSplitSelectAnimComplete}
-     *
-     * TODO: b/394268248 - desk needs to be deactivated.
      */
-    private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
+    private fun addMoveToSplitChanges(
+        wct: WindowContainerTransaction,
+        taskInfo: RunningTaskInfo,
+        deskId: Int?,
+    ): RunOnTransitStart? {
         // This windowing mode is to get the transition animation started; once we complete
         // split select, we will change windowing mode to undefined and inherit from split stage.
         // Going to undefined here causes task to flicker to the top left.
@@ -2714,11 +2732,12 @@
         // want it overridden in multi-window.
         wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
 
-        performDesktopExitCleanupIfNeeded(
+        return performDesktopExitCleanupIfNeeded(
             taskId = taskInfo.taskId,
             displayId = taskInfo.displayId,
+            deskId = deskId,
             wct = wct,
-            forceToFullscreen = false,
+            forceToFullscreen = true,
             shouldEndUpAtHome = false,
         )
     }
@@ -2950,14 +2969,21 @@
                     }
                 dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState)
             } else {
+                val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
+                logV("Split requested for task=%d in desk=%d", taskInfo.taskId, deskId)
                 val wct = WindowContainerTransaction()
-                addMoveToSplitChanges(wct, taskInfo)
-                splitScreenController.requestEnterSplitSelect(
-                    taskInfo,
-                    wct,
-                    if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT,
-                    taskInfo.configuration.windowConfiguration.bounds,
-                )
+                val runOnTransitStart = addMoveToSplitChanges(wct, taskInfo, deskId)
+                val transition =
+                    splitScreenController.requestEnterSplitSelect(
+                        taskInfo,
+                        wct,
+                        if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT
+                        else SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                        taskInfo.configuration.windowConfiguration.bounds,
+                    )
+                if (transition != null) {
+                    runOnTransitStart?.invoke(transition)
+                }
             }
         }
     }
@@ -3163,25 +3189,24 @@
                 val wct = WindowContainerTransaction()
                 wct.setBounds(taskInfo.token, destinationBounds)
 
-                // TODO: b/362720497 - reparent to a specific desk within the target display.
-                // Reparent task if it has been moved to a new display.
-                if (Flags.enableConnectedDisplaysWindowDrag()) {
-                    val newDisplayId = motionEvent.getDisplayId()
-                    if (newDisplayId != taskInfo.getDisplayId()) {
-                        val displayAreaInfo =
-                            rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
-                        if (displayAreaInfo == null) {
-                            logW(
-                                "Task reparent cannot find DisplayAreaInfo for displayId=%d",
-                                newDisplayId,
-                            )
-                        } else {
-                            wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
-                        }
+                val newDisplayId = motionEvent.getDisplayId()
+                val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
+                val isCrossDisplayDrag =
+                    Flags.enableConnectedDisplaysWindowDrag() &&
+                        newDisplayId != taskInfo.getDisplayId() &&
+                        displayAreaInfo != null
+                val handler =
+                    if (isCrossDisplayDrag) {
+                        dragToDisplayTransitionHandler
+                    } else {
+                        null
                     }
+                if (isCrossDisplayDrag) {
+                    // TODO: b/362720497 - reparent to a specific desk within the target display.
+                    wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
                 }
 
-                transitions.startTransition(TRANSIT_CHANGE, wct, null)
+                transitions.startTransition(TRANSIT_CHANGE, wct, handler)
 
                 releaseVisualIndicator()
             }
@@ -3603,27 +3628,11 @@
                     controller,
                     { c ->
                         run {
-                            c.taskRepository.addDeskChangeListener(
-                                deskChangeListener,
-                                c.mainExecutor,
-                            )
-                            c.taskRepository.addVisibleTasksListener(
-                                visibleTasksListener,
-                                c.mainExecutor,
-                            )
-                            c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
-                            c.desktopModeEnterExitTransitionListener =
-                                desktopModeEntryExitTransitionListener
+                            syncInitialState(c)
+                            registerListeners(c)
                         }
                     },
-                    { c ->
-                        run {
-                            c.taskRepository.removeDeskChangeListener(deskChangeListener)
-                            c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
-                            c.taskbarDesktopTaskListener = null
-                            c.desktopModeEnterExitTransitionListener = null
-                        }
-                    },
+                    { c -> run { unregisterListeners(c) } },
                 )
         }
 
@@ -3719,6 +3728,31 @@
                 c.startLaunchIntentTransition(intent, options, displayId)
             }
         }
+
+        private fun syncInitialState(c: DesktopTasksController) {
+            remoteListener.call { l ->
+                // TODO: b/393962589 - implement desks limit.
+                val canCreateDesks = true
+                l.onListenerConnected(
+                    c.taskRepository.getDeskDisplayStateForRemote(),
+                    canCreateDesks,
+                )
+            }
+        }
+
+        private fun registerListeners(c: DesktopTasksController) {
+            c.taskRepository.addDeskChangeListener(deskChangeListener, c.mainExecutor)
+            c.taskRepository.addVisibleTasksListener(visibleTasksListener, c.mainExecutor)
+            c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+            c.desktopModeEnterExitTransitionListener = desktopModeEntryExitTransitionListener
+        }
+
+        private fun unregisterListeners(c: DesktopTasksController) {
+            c.taskRepository.removeDeskChangeListener(deskChangeListener)
+            c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
+            c.taskbarDesktopTaskListener = null
+            c.desktopModeEnterExitTransitionListener = null
+        }
     }
 
     private fun logV(msg: String, vararg arguments: Any?) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt
new file mode 100644
index 0000000..d51576a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.transition.Transitions
+
+/** Handles the transition to drag a window to another display by dragging the caption. */
+class DragToDisplayTransitionHandler : Transitions.TransitionHandler {
+    override fun handleRequest(
+        transition: IBinder,
+        request: TransitionRequestInfo,
+    ): WindowContainerTransaction? {
+        return null
+    }
+
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: Transitions.TransitionFinishCallback,
+    ): Boolean {
+        for (change in info.changes) {
+            val sc = change.leash
+            val endBounds = change.endAbsBounds
+            val endPosition = change.endRelOffset
+            startTransaction
+                .setWindowCrop(sc, endBounds.width(), endBounds.height())
+                .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
+            finishTransaction
+                .setWindowCrop(sc, endBounds.width(), endBounds.height())
+                .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
+        }
+
+        startTransaction.apply()
+        finishCallback.onTransitionFinished(null)
+        return true
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index ba30d92..10b23fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -54,6 +54,7 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.ArrayMap;
@@ -580,10 +581,13 @@
      * @param wct transaction to apply if this is a valid request
      * @param splitPosition the split position this task should move to
      * @param taskBounds current freeform bounds of the task entering split
+     *
+     * @return the token of the transition that started as a result of entering split select.
      */
-    public void requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+    @Nullable
+    public IBinder requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
             WindowContainerTransaction wct, int splitPosition, Rect taskBounds) {
-        mStageCoordinator.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds);
+        return mStageCoordinator.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 77a7c54..0438d16 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -122,6 +122,7 @@
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.widget.Toast;
+import android.window.DesktopExperienceFlags;
 import android.window.DisplayAreaInfo;
 import android.window.RemoteTransition;
 import android.window.TransitionInfo;
@@ -219,6 +220,7 @@
     private final Context mContext;
     private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>();
     private final Set<SplitScreen.SplitSelectListener> mSelectListeners = new HashSet<>();
+    private final Transitions mTransitions;
     private final DisplayController mDisplayController;
     private final DisplayImeController mDisplayImeController;
     private final DisplayInsetsController mDisplayInsetsController;
@@ -419,6 +421,7 @@
                     iconProvider,
                     mWindowDecorViewModel, STAGE_TYPE_SIDE);
         }
+        mTransitions = transitions;
         mDisplayController = displayController;
         mDisplayImeController = displayImeController;
         mDisplayInsetsController = displayInsetsController;
@@ -455,6 +458,7 @@
         mTaskOrganizer = taskOrganizer;
         mMainStage = mainStage;
         mSideStage = sideStage;
+        mTransitions = transitions;
         mDisplayController = displayController;
         mDisplayImeController = displayImeController;
         mDisplayInsetsController = displayInsetsController;
@@ -660,16 +664,22 @@
         return mLogger;
     }
 
-    void requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+    @Nullable
+    IBinder requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
             WindowContainerTransaction wct, int splitPosition, Rect taskBounds) {
         boolean enteredSplitSelect = false;
         for (SplitScreen.SplitSelectListener listener : mSelectListeners) {
             enteredSplitSelect |= listener.onRequestEnterSplitSelect(taskInfo, splitPosition,
                     taskBounds);
         }
-        if (enteredSplitSelect) {
-            mTaskOrganizer.applyTransaction(wct);
+        if (!enteredSplitSelect) {
+            return null;
         }
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
+            mTaskOrganizer.applyTransaction(wct);
+            return null;
+        }
+        return mTransitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null);
     }
 
     void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index e9c6ade..3652a16 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -67,7 +67,6 @@
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
 import static com.android.wm.shell.transition.DefaultSurfaceAnimator.buildSurfaceAnimation;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.isCoveredByOpaqueFullscreenChange;
@@ -543,21 +542,9 @@
                         backgroundColorForTransition);
 
                 if (!isTask && a.getExtensionEdges() != 0x0) {
-                    if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) {
-                        startTransaction.setEdgeExtensionEffect(
-                                change.getLeash(), a.getExtensionEdges());
-                        finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0);
-                    } else {
-                        if (!TransitionUtil.isOpeningType(mode)) {
-                            // Can screenshot now (before startTransaction is applied)
-                            edgeExtendWindow(change, a, startTransaction, finishTransaction);
-                        } else {
-                            // Need to screenshot after startTransaction is applied otherwise
-                            // activity may not be visible or ready yet.
-                            postStartTransactionCallbacks
-                                    .add(t -> edgeExtendWindow(change, a, t, finishTransaction));
-                        }
-                    }
+                    startTransaction.setEdgeExtensionEffect(
+                            change.getLeash(), a.getExtensionEdges());
+                    finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0);
                 }
 
                 final Rect clipRect = TransitionUtil.isClosingType(mode)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index 7984bce..edfb560 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -26,7 +26,6 @@
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW;
 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
-import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -39,20 +38,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.WindowConfiguration;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.view.animation.Animation;
-import android.view.animation.Transformation;
-import android.window.ScreenCapture;
 import android.window.TransitionInfo;
 
 import com.android.internal.R;
@@ -317,129 +306,6 @@
     }
 
     /**
-     * Adds edge extension surface to the given {@code change} for edge extension animation.
-     */
-    public static void edgeExtendWindow(@NonNull TransitionInfo.Change change,
-            @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction) {
-        // Do not create edge extension surface for transfer starting window change.
-        // The app surface could be empty thus nothing can draw on the hardware renderer, which will
-        // block this thread when calling Surface#unlockCanvasAndPost.
-        if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
-            return;
-        }
-        final Transformation transformationAtStart = new Transformation();
-        a.getTransformationAt(0, transformationAtStart);
-        final Transformation transformationAtEnd = new Transformation();
-        a.getTransformationAt(1, transformationAtEnd);
-
-        // We want to create an extension surface that is the maximal size and the animation will
-        // take care of cropping any part that overflows.
-        final Insets maxExtensionInsets = Insets.min(
-                transformationAtStart.getInsets(), transformationAtEnd.getInsets());
-
-        final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
-                change.getEndAbsBounds().height());
-        final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
-                change.getEndAbsBounds().width());
-        if (maxExtensionInsets.left < 0) {
-            final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
-            final Rect extensionRect = new Rect(0, 0,
-                    -maxExtensionInsets.left, targetSurfaceHeight);
-            final int xPos = maxExtensionInsets.left;
-            final int yPos = 0;
-            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
-                    "Left Edge Extension", startTransaction, finishTransaction);
-        }
-
-        if (maxExtensionInsets.top < 0) {
-            final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
-            final Rect extensionRect = new Rect(0, 0,
-                    targetSurfaceWidth, -maxExtensionInsets.top);
-            final int xPos = 0;
-            final int yPos = maxExtensionInsets.top;
-            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
-                    "Top Edge Extension", startTransaction, finishTransaction);
-        }
-
-        if (maxExtensionInsets.right < 0) {
-            final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
-                    targetSurfaceWidth, targetSurfaceHeight);
-            final Rect extensionRect = new Rect(0, 0,
-                    -maxExtensionInsets.right, targetSurfaceHeight);
-            final int xPos = targetSurfaceWidth;
-            final int yPos = 0;
-            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
-                    "Right Edge Extension", startTransaction, finishTransaction);
-        }
-
-        if (maxExtensionInsets.bottom < 0) {
-            final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
-                    targetSurfaceWidth, targetSurfaceHeight);
-            final Rect extensionRect = new Rect(0, 0,
-                    targetSurfaceWidth, -maxExtensionInsets.bottom);
-            final int xPos = maxExtensionInsets.left;
-            final int yPos = targetSurfaceHeight;
-            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
-                    "Bottom Edge Extension", startTransaction, finishTransaction);
-        }
-    }
-
-    /**
-     * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension
-     * animation.
-     */
-    private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend,
-            @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos,
-            @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction) {
-        final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
-                .setName(layerName)
-                .setParent(surfaceToExtend)
-                .setHidden(true)
-                .setCallsite("TransitionAnimationHelper#createExtensionSurface")
-                .setOpaque(true)
-                .setBufferSize(extensionRect.width(), extensionRect.height())
-                .build();
-
-        final ScreenCapture.LayerCaptureArgs captureArgs =
-                new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend)
-                        .setSourceCrop(edgeBounds)
-                        .setFrameScale(1)
-                        .setPixelFormat(PixelFormat.RGBA_8888)
-                        .setChildrenOnly(true)
-                        .setAllowProtected(false)
-                        .setCaptureSecureLayers(true)
-                        .build();
-        final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer =
-                ScreenCapture.captureLayers(captureArgs);
-
-        if (edgeBuffer == null) {
-            ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
-                    "Failed to capture edge of window.");
-            return null;
-        }
-
-        final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
-                Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
-        final Paint paint = new Paint();
-        paint.setShader(shader);
-
-        final Surface surface = new Surface(edgeExtensionLayer);
-        final Canvas c = surface.lockHardwareCanvas();
-        c.drawRect(extensionRect, paint);
-        surface.unlockCanvasAndPost(c);
-        surface.release();
-
-        startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
-        startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
-        startTransaction.setVisibility(edgeExtensionLayer, true);
-        finishTransaction.remove(edgeExtensionLayer);
-
-        return edgeExtensionLayer;
-    }
-
-    /**
      * Returns whether there is an opaque fullscreen Change positioned in front of the given Change
      * in the given TransitionInfo.
      */
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 ff50672..ad2e23c 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
@@ -50,6 +50,7 @@
 import androidx.core.view.isGone
 import com.android.window.flags.Flags
 import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.ContextUtils.isRtl
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread
 import com.android.wm.shell.shared.annotations.ShellMainThread
 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
@@ -60,6 +61,8 @@
 import com.android.wm.shell.windowdecor.common.DecorThemeUtil
 import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
 import com.android.wm.shell.windowdecor.common.calculateMenuPosition
+import com.android.wm.shell.windowdecor.common.DrawableInsets
+import com.android.wm.shell.windowdecor.common.createRippleDrawable
 import com.android.wm.shell.windowdecor.extension.isFullscreen
 import com.android.wm.shell.windowdecor.extension.isMultiWindow
 import com.android.wm.shell.windowdecor.extension.isPinned
@@ -71,6 +74,7 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
+
 /**
  * Handle menu opened when the appropriate button is clicked on.
  *
@@ -467,6 +471,33 @@
         val rootView = LayoutInflater.from(context)
             .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View
 
+        private val windowingButtonRippleRadius = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_handle_menu_windowing_action_ripple_radius)
+        private val windowingButtonDrawableInsets = DrawableInsets(
+            vertical = context.resources
+                .getDimensionPixelSize(
+                    R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+            horizontal = context.resources
+                .getDimensionPixelSize(
+                    R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base)
+        )
+        private val windowingButtonDrawableInsetsLeft = DrawableInsets(
+            vertical = context.resources
+                .getDimensionPixelSize(
+                    R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+            horizontalLeft = context.resources
+                .getDimensionPixelSize(
+                    R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift),
+        )
+        private val windowingButtonDrawableInsetsRight = DrawableInsets(
+            vertical = context.resources
+                .getDimensionPixelSize(
+                    R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+            horizontalRight = context.resources
+                .getDimensionPixelSize(
+                    R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift)
+        )
+
         // App Info Pill.
         private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill)
         private val collapseMenuButton = appInfoPill.requireViewById<HandleMenuImageButton>(
@@ -708,6 +739,49 @@
             desktopBtn.isSelected = taskInfo.isFreeform
             desktopBtn.isEnabled = !taskInfo.isFreeform
             desktopBtn.imageTintList = style.windowingButtonColor
+
+            val startInsets = if (context.isRtl) {
+                windowingButtonDrawableInsetsRight
+            } else {
+                windowingButtonDrawableInsetsLeft
+            }
+            val endInsets = if (context.isRtl) {
+                windowingButtonDrawableInsetsLeft
+            } else {
+                windowingButtonDrawableInsetsRight
+            }
+
+            fullscreenBtn.apply {
+                background = createRippleDrawable(
+                    color = style.textColor,
+                    cornerRadius = windowingButtonRippleRadius,
+                    drawableInsets = startInsets
+                )
+            }
+
+            splitscreenBtn.apply {
+                background = createRippleDrawable(
+                    color = style.textColor,
+                    cornerRadius = windowingButtonRippleRadius,
+                    drawableInsets = windowingButtonDrawableInsets
+                )
+            }
+
+            floatingBtn.apply {
+                background = createRippleDrawable(
+                    color = style.textColor,
+                    cornerRadius = windowingButtonRippleRadius,
+                    drawableInsets = windowingButtonDrawableInsets
+                )
+            }
+
+            desktopBtn.apply {
+                background = createRippleDrawable(
+                    color = style.textColor,
+                    cornerRadius = windowingButtonRippleRadius,
+                    drawableInsets = endInsets
+                )
+            }
         }
 
         private fun bindMoreActionsPill(style: MenuStyle) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
index c6cb62d..1b0e0f70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
@@ -363,10 +363,11 @@
         dragEventListeners.remove(dragEventListener)
     }
 
-    override fun onTopologyChanged(topology: DisplayTopology) {
+    override fun onTopologyChanged(topology: DisplayTopology?) {
         // TODO: b/383069173 - Cancel window drag when topology changes happen during drag.
 
         displayIds.clear()
+        if (topology == null) return
         val displayBounds = topology.getAbsoluteBounds()
         displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) })
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index 7af6b8e..5bd4228 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -225,7 +225,7 @@
             val veilAnimT = surfaceControlTransactionSupplier.get()
             val iconAnimT = surfaceControlTransactionSupplier.get()
             veilAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
-                duration = RESIZE_ALPHA_DURATION
+                duration = VEIL_ENTRY_ALPHA_ANIMATION_DURATION
                 addUpdateListener {
                     veilAnimT.setAlpha(background, animatedValue as Float)
                             .apply()
@@ -243,7 +243,8 @@
                 })
             }
             iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
-                duration = RESIZE_ALPHA_DURATION
+                duration = ICON_ALPHA_ANIMATION_DURATION
+                startDelay = ICON_ENTRY_DELAY
                 addUpdateListener {
                     iconAnimT.setAlpha(icon, animatedValue as Float)
                             .apply()
@@ -387,23 +388,38 @@
         if (background == null || icon == null) return
 
         veilAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
-            duration = RESIZE_ALPHA_DURATION
+            duration = VEIL_EXIT_ALPHA_ANIMATION_DURATION
+            startDelay = VEIL_EXIT_DELAY
             addUpdateListener {
                 surfaceControlTransactionSupplier.get()
                         .setAlpha(background, animatedValue as Float)
-                        .setAlpha(icon, animatedValue as Float)
                         .apply()
             }
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
                     surfaceControlTransactionSupplier.get()
                             .hide(background)
-                            .hide(icon)
                             .apply()
                 }
             })
         }
+        iconAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
+            duration = ICON_ALPHA_ANIMATION_DURATION
+            addUpdateListener {
+                surfaceControlTransactionSupplier.get()
+                    .setAlpha(icon, animatedValue as Float)
+                    .apply()
+            }
+            addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    surfaceControlTransactionSupplier.get()
+                        .hide(icon)
+                        .apply()
+                }
+            })
+        }
         veilAnimator?.start()
+        iconAnimator?.start()
         isVisible = false
     }
 
@@ -451,7 +467,11 @@
 
     companion object {
         private const val TAG = "ResizeVeil"
-        private const val RESIZE_ALPHA_DURATION = 100L
+        private const val ICON_ALPHA_ANIMATION_DURATION = 50L
+        private const val VEIL_ENTRY_ALPHA_ANIMATION_DURATION = 50L
+        private const val VEIL_EXIT_ALPHA_ANIMATION_DURATION = 200L
+        private const val ICON_ENTRY_DELAY = 33L
+        private const val VEIL_EXIT_DELAY = 33L
         private const val VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL
 
         /** The background is a child of the veil container layer and goes at the bottom.  */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
new file mode 100644
index 0000000..e18239d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.wm.shell.windowdecor.common
+
+import android.annotation.ColorInt
+import android.graphics.Color
+import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import com.android.wm.shell.windowdecor.common.OPACITY_11
+import com.android.wm.shell.windowdecor.common.OPACITY_15
+import android.content.res.ColorStateList
+
+/**
+ * Represents drawable insets, specifying the number of pixels to inset a drawable from its bounds.
+ */
+data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
+    constructor(vertical: Int = 0, horizontal: Int = 0) :
+            this(horizontal, vertical, horizontal, vertical)
+    constructor(vertical: Int = 0, horizontalLeft: Int = 0, horizontalRight: Int = 0) :
+            this(horizontalLeft, vertical, horizontalRight, vertical)
+}
+
+/**
+ * Replaces the alpha component of a color with the given alpha value.
+ */
+@ColorInt
+fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
+    return Color.argb(
+        alpha,
+        Color.red(color),
+        Color.green(color),
+        Color.blue(color)
+    )
+}
+
+/**
+ * Creates a RippleDrawable with specified color, corner radius, and insets.
+ */
+fun createRippleDrawable(
+            @ColorInt color: Int,
+            cornerRadius: Int,
+            drawableInsets: DrawableInsets,
+): RippleDrawable {
+    return RippleDrawable(
+        ColorStateList(
+            arrayOf(
+                intArrayOf(android.R.attr.state_hovered),
+                intArrayOf(android.R.attr.state_pressed),
+                intArrayOf(),
+            ),
+            intArrayOf(
+                replaceColorAlpha(color, OPACITY_11),
+                replaceColorAlpha(color, OPACITY_15),
+                Color.TRANSPARENT,
+            )
+        ),
+        null /* content */,
+        LayerDrawable(arrayOf(
+            ShapeDrawable().apply {
+                shape = RoundRectShape(
+                    FloatArray(8) { cornerRadius.toFloat() },
+                    null /* inset */,
+                    null /* innerRadii */
+                )
+                paint.color = Color.WHITE
+            }
+        )).apply {
+            require(numberOfLayers == 1) { "Must only contain one layer" }
+            setLayerInset(0 /* index */,
+                drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
+        }
+    )
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 870c894..eb8b617 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -61,6 +61,8 @@
 import com.android.wm.shell.windowdecor.common.OPACITY_55
 import com.android.wm.shell.windowdecor.common.OPACITY_65
 import com.android.wm.shell.windowdecor.common.Theme
+import com.android.wm.shell.windowdecor.common.DrawableInsets
+import com.android.wm.shell.windowdecor.common.createRippleDrawable
 import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
 import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
 
@@ -635,61 +637,10 @@
         )
     }
 
-    @ColorInt
-    private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
-        return Color.argb(
-            alpha,
-            Color.red(color),
-            Color.green(color),
-            Color.blue(color)
-        )
-    }
-
-    private fun createRippleDrawable(
-        @ColorInt color: Int,
-        cornerRadius: Int,
-        drawableInsets: DrawableInsets,
-    ): RippleDrawable {
-        return RippleDrawable(
-            ColorStateList(
-                arrayOf(
-                    intArrayOf(android.R.attr.state_hovered),
-                    intArrayOf(android.R.attr.state_pressed),
-                    intArrayOf(),
-                ),
-                intArrayOf(
-                    replaceColorAlpha(color, OPACITY_11),
-                    replaceColorAlpha(color, OPACITY_15),
-                    Color.TRANSPARENT
-                )
-            ),
-            null /* content */,
-            LayerDrawable(arrayOf(
-                ShapeDrawable().apply {
-                    shape = RoundRectShape(
-                        FloatArray(8) { cornerRadius.toFloat() },
-                        null /* inset */,
-                        null /* innerRadii */
-                    )
-                    paint.color = Color.WHITE
-                }
-            )).apply {
-                require(numberOfLayers == 1) { "Must only contain one layer" }
-                setLayerInset(0 /* index */,
-                    drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
-            }
-        )
-    }
-
     private enum class SizeToggleDirection {
         MAXIMIZE, RESTORE
     }
 
-    private data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
-        constructor(vertical: Int = 0, horizontal: Int = 0) :
-                this(horizontal, vertical, horizontal, vertical)
-    }
-
     private data class Header(
         val type: Type,
         val appTheme: Theme,
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
index 2800839..d82c066 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
@@ -18,9 +18,9 @@
 
 import android.tools.NavBar
 import android.tools.Rotation
-import com.android.internal.R
 import com.android.window.flags.Flags
 import com.android.wm.shell.Utils
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
@@ -42,8 +42,8 @@
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
         // Skip the test when the drag-to-maximize is enabled on this device.
-        Assume.assumeFalse(Flags.enableDragToMaximize() &&
-            instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
+        Assume.assumeFalse(
+            DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context))
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         testApp.enterDesktopMode(wmHelper, device)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
index 60a0fb5..675b63c 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
@@ -23,12 +23,12 @@
 import android.tools.traces.parsers.WindowManagerStateHelper
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
-import com.android.internal.R
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.window.flags.Flags
 import com.android.wm.shell.Utils
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
@@ -54,8 +54,8 @@
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
         // Skip the test when the drag-to-maximize is disabled on this device.
-        Assume.assumeTrue(Flags.enableDragToMaximize() &&
-            instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
+        Assume.assumeTrue(
+            DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context))
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         ChangeDisplayOrientationRule.setRotation(rotation)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
index 81c46f1..b9a5e4a9 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
@@ -25,6 +25,7 @@
 import android.tools.flicker.rules.ChangeDisplayOrientationRule
 import android.tools.traces.parsers.WindowManagerStateHelper
 import android.util.DisplayMetrics
+import android.window.DesktopExperienceFlags
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
@@ -64,7 +65,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
-        Assume.assumeTrue(Flags.enableDisplayWindowingModeSwitching())
+        Assume.assumeTrue(DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue)
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         ChangeDisplayOrientationRule.setRotation(rotation)
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 7f48499..e39fa3a 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -22,7 +22,6 @@
 import android.tools.flicker.legacy.LegacyFlickerTest
 import android.tools.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.traces.component.ComponentNameMatcher
-import android.tools.traces.component.EdgeExtensionComponentMatcher
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
@@ -99,7 +98,6 @@
                         ComponentNameMatcher.SPLASH_SCREEN,
                         ComponentNameMatcher.SNAPSHOT,
                         ComponentNameMatcher.IME_SNAPSHOT,
-                        EdgeExtensionComponentMatcher(),
                         magnifierLayer,
                         popupWindowLayer
                     )
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
index 0ff7230..f0c97d3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -101,7 +101,7 @@
     private fun testDisplayWindowingModeSwitch(
         defaultWindowingMode: Int,
         extendedDisplayEnabled: Boolean,
-        expectTransition: Boolean,
+        expectToSwitch: Boolean,
     ) {
         defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
         whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
@@ -113,10 +113,14 @@
 
         settingsSession.use {
             connectExternalDisplay()
-            defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+            if (expectToSwitch) {
+                // Assumes [connectExternalDisplay] properly triggered the switching transition.
+                // Will verify the transition later along with [disconnectExternalDisplay].
+                defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+            }
             disconnectExternalDisplay()
 
-            if (expectTransition) {
+            if (expectToSwitch) {
                 val arg = argumentCaptor<WindowContainerTransaction>()
                 verify(transitions, times(2))
                     .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
@@ -139,7 +143,7 @@
         testDisplayWindowingModeSwitch(
             defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
             extendedDisplayEnabled = false,
-            expectTransition = false,
+            expectToSwitch = false,
         )
     }
 
@@ -148,7 +152,7 @@
         testDisplayWindowingModeSwitch(
             defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
             extendedDisplayEnabled = true,
-            expectTransition = true,
+            expectToSwitch = true,
         )
     }
 
@@ -157,7 +161,7 @@
         testDisplayWindowingModeSwitch(
             defaultWindowingMode = WINDOWING_MODE_FREEFORM,
             extendedDisplayEnabled = true,
-            expectTransition = false,
+            expectToSwitch = false,
         )
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 93eb396..04acaef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -261,6 +261,7 @@
     @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver
     @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var mockDisplayContext: Context
+    @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler
 
     private lateinit var controller: DesktopTasksController
     private lateinit var shellInit: ShellInit
@@ -431,6 +432,7 @@
             desksTransitionsObserver,
             userProfileContexts,
             desktopModeCompatPolicy,
+            dragToDisplayTransitionHandler,
         )
 
     @After
@@ -2069,6 +2071,21 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun moveToFullscreen_fromDeskWithMultipleTasks_deactivatesDesk() {
+        val deskId = 1
+        taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+        taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+        val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+        val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+
+        controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        verify(desksOrganizer).deactivateDesk(wct, deskId = deskId)
+    }
+
+    @Test
     fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
         val task = setUpFreeformTask()
         val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -2278,7 +2295,10 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    @DisableFlags(
+        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+    )
     fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
         val homeTask = setUpHomeTask()
         val task1 = setUpFreeformTask()
@@ -2305,29 +2325,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
-    fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
-        val homeTask = setUpHomeTask()
-        val task1 = setUpFreeformTask()
-        // Setup task2
-        setUpFreeformTask()
-
-        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
-        assertNotNull(tdaInfo).configuration.windowConfiguration.windowingMode =
-            WINDOWING_MODE_FULLSCREEN
-
-        controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
-
-        val wct = getLatestExitDesktopWct()
-        val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
-        assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
-        verify(desktopModeEnterExitTransitionListener)
-            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
-        // Does not remove wallpaper activity, as desktop still has a visible desktop task
-        wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false))
-    }
-
-    @Test
     fun moveToFullscreen_nonExistentTask_doesNothing() {
         controller.moveToFullscreen(999, transitionSource = UNKNOWN)
         verifyExitDesktopWCTNotExecuted()
@@ -4455,7 +4452,10 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    @DisableFlags(
+        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+    )
     fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
         val homeTask = setUpHomeTask()
         val task1 = setUpFreeformTask()
@@ -4480,27 +4480,6 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
-    fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
-        val homeTask = setUpHomeTask()
-        val task1 = setUpFreeformTask()
-        val task2 = setUpFreeformTask()
-        val task3 = setUpFreeformTask()
-
-        task1.isFocused = false
-        task2.isFocused = true
-        task3.isFocused = false
-        controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
-        val wct = getLatestExitDesktopWct()
-        val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
-        assertThat(taskChange.windowingMode)
-            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
-        // Does not remove wallpaper activity
-        wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null))
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() {
         val homeTask = setUpHomeTask()
         val task1 = setUpFreeformTask()
@@ -5031,7 +5010,7 @@
                 Mockito.argThat { wct ->
                     return@argThat wct.hierarchyOps[0].isReparent
                 },
-                eq(null),
+                eq(dragToDisplayTransitionHandler),
             )
     }
 
@@ -5225,6 +5204,10 @@
     }
 
     @Test
+    @DisableFlags(
+        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+    )
     fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() {
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
@@ -5249,6 +5232,24 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun enterSplit_wasInDesk_deactivatesDesk() {
+        val deskId = 5
+        taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+        taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+        val transition = Binder()
+        whenever(splitScreenController.requestEnterSplitSelect(eq(task), any(), any(), any()))
+            .thenReturn(transition)
+
+        controller.requestSplit(task, leftOrTop = false)
+
+        verify(desksOrganizer).deactivateDesk(any(), eq(deskId))
+        verify(desksTransitionsObserver)
+            .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId))
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
     fun newWindow_fromFullscreenOpensInSplit() {
         setUpLandscapeDisplay()
@@ -6601,6 +6602,7 @@
         bounds: Rect? = null,
         active: Boolean = true,
         background: Boolean = false,
+        deskId: Int? = null,
     ): RunningTaskInfo {
         val task = createFreeformTask(displayId, bounds)
         val activityInfo = ActivityInfo()
@@ -6613,7 +6615,11 @@
         } else {
             whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
         }
-        taskRepository.addTask(displayId, task.taskId, isVisible = active)
+        if (deskId != null) {
+            taskRepository.addTaskToDesk(displayId, deskId, task.taskId, isVisible = active)
+        } else {
+            taskRepository.addTask(displayId, task.taskId, isVisible = active)
+        }
         if (!background) {
             runningTasks.add(task)
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt
new file mode 100644
index 0000000..51c3029
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import com.android.wm.shell.transition.Transitions
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Test class for {@link DragToDisplayTransitionHandler}
+ *
+ * Usage: atest WMShellUnitTests:DragToDisplayTransitionHandlerTest
+ */
+class DragToDisplayTransitionHandlerTest {
+    private lateinit var handler: DragToDisplayTransitionHandler
+    private val mockTransition: IBinder = mock()
+    private val mockRequestInfo: TransitionRequestInfo = mock()
+    private val mockTransitionInfo: TransitionInfo = mock()
+    private val mockStartTransaction: SurfaceControl.Transaction = mock()
+    private val mockFinishTransaction: SurfaceControl.Transaction = mock()
+    private val mockFinishCallback: Transitions.TransitionFinishCallback = mock()
+
+    @Before
+    fun setUp() {
+        handler = DragToDisplayTransitionHandler()
+        whenever(mockStartTransaction.setWindowCrop(any(), any(), any()))
+            .thenReturn(mockStartTransaction)
+        whenever(mockFinishTransaction.setWindowCrop(any(), any(), any()))
+            .thenReturn(mockFinishTransaction)
+    }
+
+    @Test
+    fun handleRequest_anyRequest_returnsNull() {
+        val result = handler.handleRequest(mockTransition, mockRequestInfo)
+        assert(result == null)
+    }
+
+    @Test
+    fun startAnimation_verifyTransformationsApplied() {
+        val mockChange1 = mock<TransitionInfo.Change>()
+        val leash1 = mock<SurfaceControl>()
+        val endBounds1 = Rect(0, 0, 50, 50)
+        val endPosition1 = Point(5, 5)
+
+        whenever(mockChange1.leash).doReturn(leash1)
+        whenever(mockChange1.endAbsBounds).doReturn(endBounds1)
+        whenever(mockChange1.endRelOffset).doReturn(endPosition1)
+
+        val mockChange2 = mock<TransitionInfo.Change>()
+        val leash2 = mock<SurfaceControl>()
+        val endBounds2 = Rect(100, 100, 200, 150)
+        val endPosition2 = Point(15, 25)
+
+        whenever(mockChange2.leash).doReturn(leash2)
+        whenever(mockChange2.endAbsBounds).doReturn(endBounds2)
+        whenever(mockChange2.endRelOffset).doReturn(endPosition2)
+
+        whenever(mockTransitionInfo.changes).doReturn(listOf(mockChange1, mockChange2))
+
+        handler.startAnimation(
+            mockTransition,
+            mockTransitionInfo,
+            mockStartTransaction,
+            mockFinishTransaction,
+            mockFinishCallback,
+        )
+
+        verify(mockStartTransaction).setWindowCrop(leash1, endBounds1.width(), endBounds1.height())
+        verify(mockStartTransaction)
+            .setPosition(leash1, endPosition1.x.toFloat(), endPosition1.y.toFloat())
+        verify(mockStartTransaction).setWindowCrop(leash2, endBounds2.width(), endBounds2.height())
+        verify(mockStartTransaction)
+            .setPosition(leash2, endPosition2.x.toFloat(), endPosition2.y.toFloat())
+        verify(mockStartTransaction).apply()
+        verify(mockFinishCallback).onTransitionFinished(null)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 10c2862..e9c4c31 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -38,6 +38,8 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -48,6 +50,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -58,6 +61,7 @@
 import android.app.PendingIntent;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -196,6 +200,7 @@
         when(token.asBinder()).thenReturn(mBinder);
         when(mRunningTaskInfo.getToken()).thenReturn(token);
         when(mTaskOrganizer.getRunningTaskInfo(mTaskId)).thenReturn(mRunningTaskInfo);
+        when(mTaskOrganizer.startNewTransition(anyInt(), any())).thenReturn(new Binder());
         when(mRootTDAOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(mDisplayAreaInfo);
 
         when(mSplitLayout.getTopLeftBounds()).thenReturn(mBounds1);
@@ -557,6 +562,60 @@
         assertFalse(c != null && c.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    public void testRequestEnterSplit_didNotEnterSplitSelect_doesNotApplyTransaction() {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mStageCoordinator.registerSplitSelectListener(
+                new TestSplitSelectListener(/* alwaysEnter = */ false));
+
+        final IBinder transition = mStageCoordinator.requestEnterSplitSelect(mRunningTaskInfo, wct,
+                SPLIT_POSITION_TOP_OR_LEFT, new Rect(0, 0, 100, 100));
+
+        assertNull(transition);
+        verify(mTaskOrganizer, never()).applyTransaction(wct);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    public void testRequestEnterSplit_enteredSplitSelect_appliesTransaction() {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mStageCoordinator.registerSplitSelectListener(
+                new TestSplitSelectListener(/* alwaysEnter = */ true));
+
+        final IBinder transition = mStageCoordinator.requestEnterSplitSelect(mRunningTaskInfo, wct,
+                SPLIT_POSITION_TOP_OR_LEFT, new Rect(0, 0, 100, 100));
+
+        assertNull(transition);
+        verify(mTaskOrganizer).applyTransaction(wct);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    public void testRequestEnterSplit_didNotEnterSplitSelect_doesNotStartTransition() {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mStageCoordinator.registerSplitSelectListener(
+                new TestSplitSelectListener(/* alwaysEnter = */ false));
+
+        final IBinder transition = mStageCoordinator.requestEnterSplitSelect(mRunningTaskInfo, wct,
+                SPLIT_POSITION_TOP_OR_LEFT, new Rect(0, 0, 100, 100));
+
+        assertNull(transition);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    public void testRequestEnterSplit_enteredSplitSelect_startsTransition() {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mStageCoordinator.registerSplitSelectListener(
+                new TestSplitSelectListener(/* alwaysEnter = */ true));
+
+        final IBinder transition = mStageCoordinator.requestEnterSplitSelect(mRunningTaskInfo, wct,
+                SPLIT_POSITION_TOP_OR_LEFT, new Rect(0, 0, 100, 100));
+
+        assertNotNull(transition);
+    }
+
     private Transitions createTestTransitions() {
         ShellInit shellInit = new ShellInit(mMainExecutor);
         final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
@@ -566,4 +625,18 @@
         shellInit.init();
         return t;
     }
+
+    private static class TestSplitSelectListener implements SplitScreen.SplitSelectListener {
+        private final boolean mAlwaysEnter;
+
+        TestSplitSelectListener(boolean alwaysEnter) {
+            mAlwaysEnter = alwaysEnter;
+        }
+
+        @Override
+        public boolean onRequestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+                int splitPosition, Rect taskBounds) {
+            return mAlwaysEnter;
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 6f73db0..6773307 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -1316,9 +1316,11 @@
                         mTransactionPool, createTestDisplayController(), mMainExecutor,
                         mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
                         mock(FocusTransitionObserver.class));
+        final RecentTasksController mockRecentsTaskController = mock(RecentTasksController.class);
+        doReturn(mContext).when(mockRecentsTaskController).getContext();
         final RecentsTransitionHandler recentsHandler =
                 new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
-                        mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
+                        mockRecentsTaskController, mock(HomeTransitionObserver.class));
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
         shellInit.init();
 
diff --git a/location/java/android/location/GnssMeasurement.java b/location/java/android/location/GnssMeasurement.java
index 200d4ef..6ae73a2 100644
--- a/location/java/android/location/GnssMeasurement.java
+++ b/location/java/android/location/GnssMeasurement.java
@@ -1484,6 +1484,10 @@
      * in an open sky test - the important aspect of this output is that changes in this value are
      * indicative of changes on input signal power in the frequency band for this measurement.
      *
+     * <p> This field is part of the GnssMeasurement object so it is only reported when the GNSS
+     * measurement is reported. E.g., when a GNSS signal is too weak to be acquired, the AGC value
+     * is not reported.
+     *
      * <p> The value is only available if {@link #hasAutomaticGainControlLevelDb()} is {@code true}
      *
      * @deprecated Use {@link GnssMeasurementsEvent#getGnssAutomaticGainControls()} instead.
diff --git a/location/java/android/location/GnssMeasurementsEvent.java b/location/java/android/location/GnssMeasurementsEvent.java
index 4fc2ee8..8cdfd01 100644
--- a/location/java/android/location/GnssMeasurementsEvent.java
+++ b/location/java/android/location/GnssMeasurementsEvent.java
@@ -158,6 +158,14 @@
     /**
      * Gets the collection of {@link GnssAutomaticGainControl} associated with the
      * current event.
+     *
+     * <p>This field must be reported when the GNSS measurement engine is running, even when the
+     * GnssMeasurement or GnssClock fields are not reported yet. E.g., when a GNSS signal is too
+     * weak to be acquired, the AGC value must still be reported.
+     *
+     * <p>For devices that do not support this field, an empty collection is returned. In that case,
+     * please use {@link GnssMeasurement#hasAutomaticGainControlLevelDb()}
+     * and {@link GnssMeasuremen#getAutomaticGainControlLevelDb()}.
      */
     @NonNull
     public Collection<GnssAutomaticGainControl> getGnssAutomaticGainControls() {
diff --git a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
index 8b6194f..fb89973 100644
--- a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
+++ b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
@@ -28,7 +28,6 @@
 import android.util.Log;
 
 import com.android.internal.annotations.KeepForWeakReference;
-import com.android.internal.telephony.flags.Flags;
 
 import java.util.concurrent.TimeUnit;
 
@@ -146,17 +145,12 @@
                         < emergencyExtensionMillis);
         boolean isInEmergencyCallback = false;
         boolean isInEmergencySmsMode = false;
-        if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+        PackageManager pm = mContext.getPackageManager();
+        if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
             isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+        }
+        if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
             isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
-        } else {
-            PackageManager pm = mContext.getPackageManager();
-            if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
-                isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
-            }
-            if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
-                isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
-            }
         }
         return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension
                 || isInEmergencySmsMode;
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index 6d4f0b4..846448b 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -39,3 +39,12 @@
     }
     is_exported: true
 }
+
+flag {
+    namespace: "media_projection"
+    name: "app_content_sharing"
+    description: "Enable apps to share some sub-surface"
+    bug: "379989921"
+    is_exported: true
+}
+
diff --git a/native/android/OWNERS b/native/android/OWNERS
index 3ea2d35..1e8d30d 100644
--- a/native/android/OWNERS
+++ b/native/android/OWNERS
@@ -6,8 +6,7 @@
 
 # Networking
 per-file libandroid_net.map.txt, net.c = set noparent
-per-file libandroid_net.map.txt, net.c = codewiz@google.com, jchalard@google.com, junyulai@google.com
-per-file libandroid_net.map.txt, net.c = lorenzo@google.com, reminv@google.com, satk@google.com
+per-file libandroid_net.map.txt, net.c = file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
 
 # Fonts
 per-file system_fonts.cpp = file:/graphics/java/android/graphics/fonts/OWNERS
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
index 572fc36..3113129 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
@@ -452,7 +452,7 @@
     }
 
     private void stopDiscovery() {
-        if (!mRequest.isSelfManaged()) {
+        if (mRequest != null && !mRequest.isSelfManaged()) {
             CompanionDeviceDiscoveryService.stop(this);
         }
     }
diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
index 9d037e9..806580b 100644
--- a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
+++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
@@ -17,20 +17,20 @@
 package com.android.settingslib.widget
 
 import android.content.Context
-import android.os.Build
 import android.text.TextUtils
 import android.util.AttributeSet
 import android.view.View
-import androidx.annotation.RequiresApi
 import androidx.preference.Preference
 import androidx.preference.PreferenceViewHolder
 import com.android.settingslib.widget.preference.intro.R
 
-class IntroPreference @JvmOverloads constructor(
+class IntroPreference
+@JvmOverloads
+constructor(
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
-    defStyleRes: Int = 0
+    defStyleRes: Int = 0,
 ) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin {
 
     private var isCollapsable: Boolean = true
@@ -66,9 +66,9 @@
 
     /**
      * Sets whether the summary is collapsable.
+     *
      * @param collapsable True if the summary should be collapsable, false otherwise.
      */
-    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     fun setCollapsable(collapsable: Boolean) {
         isCollapsable = collapsable
         minLines = if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES
@@ -77,9 +77,9 @@
 
     /**
      * Sets the minimum number of lines to display when collapsed.
+     *
      * @param lines The minimum number of lines.
      */
-    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     fun setMinLines(lines: Int) {
         minLines = lines.coerceIn(1, DEFAULT_MAX_LINES)
         notifyChanged()
@@ -87,9 +87,9 @@
 
     /**
      * Sets the action when clicking on the hyperlink in the text.
+     *
      * @param listener The click listener for hyperlink.
      */
-    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     fun setHyperlinkListener(listener: View.OnClickListener) {
         if (hyperlinkListener != listener) {
             hyperlinkListener = listener
@@ -99,9 +99,9 @@
 
     /**
      * Sets the action when clicking on the learn more view.
+     *
      * @param listener The click listener for learn more.
      */
-    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     fun setLearnMoreAction(listener: View.OnClickListener) {
         if (learnMoreListener != listener) {
             learnMoreListener = listener
@@ -111,9 +111,9 @@
 
     /**
      * Sets the text of learn more view.
+     *
      * @param text The text of learn more.
      */
-    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     fun setLearnMoreText(text: CharSequence) {
         if (!TextUtils.equals(learnMoreText, text)) {
             learnMoreText = text
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 1cb8005..02bef9f 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -59,8 +59,6 @@
     private val preferenceHierarchy: PreferenceHierarchy,
 ) : KeyedDataObservable<String>() {
 
-    private val mainExecutor = HandlerExecutor.main
-
     private val preferenceLifecycleContext =
         object : PreferenceLifecycleContext(context) {
             override val lifecycleScope: LifecycleCoroutineScope
@@ -88,11 +86,11 @@
     private val preferences: ImmutableMap<String, PreferenceHierarchyNode>
     private val dependencies: ImmutableMultimap<String, String>
     private val lifecycleAwarePreferences: Array<PreferenceLifecycleProvider>
-    private val storages = mutableMapOf<String, KeyedObservable<String>>()
+    private val observables = mutableMapOf<String, KeyedObservable<String>>()
 
     private val preferenceObserver: KeyedObserver<String?>
 
-    private val storageObserver =
+    private val observer =
         KeyedObserver<String> { key, reason ->
             if (DataChangeReason.isDataChange(reason)) {
                 notifyChange(key, PreferenceChangeReason.VALUE)
@@ -133,15 +131,19 @@
         this.dependencies = dependenciesBuilder.build()
         this.lifecycleAwarePreferences = lifecycleAwarePreferences.toTypedArray()
 
+        val executor = HandlerExecutor.main
         preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
-        addObserver(preferenceObserver, mainExecutor)
+        addObserver(preferenceObserver, executor)
 
         preferenceScreen.forEachRecursively {
-            it.preferenceDataStore?.findKeyValueStore()?.let { keyValueStore ->
-                val key = it.key
-                storages[key] = keyValueStore
-                keyValueStore.addObserver(key, storageObserver, mainExecutor)
-            }
+            val key = it.key ?: return@forEachRecursively
+            @Suppress("UNCHECKED_CAST")
+            val observable =
+                it.preferenceDataStore?.findKeyValueStore()
+                    ?: (preferences[key]?.metadata as? KeyedObservable<String>)
+                    ?: return@forEachRecursively
+            observables[key] = observable
+            observable.addObserver(key, observer, executor)
         }
     }
 
@@ -212,7 +214,7 @@
 
     fun onDestroy() {
         removeObserver(preferenceObserver)
-        for ((key, storage) in storages) storage.removeObserver(key, storageObserver)
+        for ((key, observable) in observables) observable.removeObserver(key, observer)
         for (preference in lifecycleAwarePreferences) {
             preference.onDestroy(preferenceLifecycleContext)
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index ebd5a1d..3625c00 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -29,6 +29,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
@@ -37,6 +38,8 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.settingslib.R;
+import com.android.settingslib.flags.Flags;
+import com.android.settingslib.utils.ThreadUtils;
 
 import java.util.Collection;
 import java.util.HashMap;
@@ -65,6 +68,7 @@
     private final android.os.Handler mReceiverHandler;
     private final UserHandle mUserHandle;
     private final Context mContext;
+    private boolean mIsWorkProfile = false;
 
     interface Handler {
         void onReceive(Context context, Intent intent, BluetoothDevice device);
@@ -140,6 +144,9 @@
         addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler());
 
         registerAdapterIntentReceiver();
+
+        UserManager userManager = context.getSystemService(UserManager.class);
+        mIsWorkProfile = userManager != null && userManager.isManagedProfile();
     }
 
     /** Register to start receiving callbacks for Bluetooth events. */
@@ -220,20 +227,32 @@
             callback.onProfileConnectionStateChanged(device, state, bluetoothProfile);
         }
 
+        if (mIsWorkProfile) {
+            Log.d(TAG, "Skip profileConnectionStateChanged for audio sharing, work profile");
+            return;
+        }
+
+        LocalBluetoothLeBroadcast broadcast = mBtManager == null ? null
+                : mBtManager.getProfileManager().getLeAudioBroadcastProfile();
+        LocalBluetoothLeBroadcastAssistant assistant = mBtManager == null ? null
+                : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
         // Trigger updateFallbackActiveDeviceIfNeeded when ASSISTANT profile disconnected when
         // audio sharing is enabled.
         if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
                 && state == BluetoothAdapter.STATE_DISCONNECTED
-                && BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
-            LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
-            if (profileManager != null
-                    && profileManager.getLeAudioBroadcastProfile() != null
-                    && profileManager.getLeAudioBroadcastProfile().isProfileReady()
-                    && profileManager.getLeAudioBroadcastAssistantProfile() != null
-                    && profileManager.getLeAudioBroadcastAssistantProfile().isProfileReady()) {
-                Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected");
-                profileManager.getLeAudioBroadcastProfile().updateFallbackActiveDeviceIfNeeded();
-            }
+                && BluetoothUtils.isAudioSharingUIAvailable(mContext)
+                && broadcast != null && assistant != null && broadcast.isProfileReady()
+                && assistant.isProfileReady()) {
+            Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected");
+            broadcast.updateFallbackActiveDeviceIfNeeded();
+        }
+        // Dispatch handleOnProfileStateChanged to local broadcast profile
+        if (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
+                && broadcast != null
+                && state == BluetoothAdapter.STATE_CONNECTED) {
+            Log.d(TAG, "dispatchProfileConnectionStateChanged to local broadcast profile");
+            var unused = ThreadUtils.postOnBackgroundThread(
+                    () -> broadcast.handleProfileConnected(device, bluetoothProfile, mBtManager));
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 31948e4..e78a692 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -719,6 +719,30 @@
         }
     }
 
+    /** Check if the {@link CachedBluetoothDevice} is a media device */
+    @WorkerThread
+    public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) {
+        if (cachedDevice == null) return false;
+        return cachedDevice.getProfiles().stream()
+                .anyMatch(
+                        profile ->
+                                profile instanceof A2dpProfile
+                                        || profile instanceof HearingAidProfile
+                                        || profile instanceof LeAudioProfile
+                                        || profile instanceof HeadsetProfile);
+    }
+
+    /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */
+    @WorkerThread
+    public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) {
+        if (cachedDevice == null) return false;
+        return cachedDevice.getProfiles().stream()
+                .anyMatch(
+                        profile ->
+                                profile instanceof LeAudioProfile
+                                        && profile.isEnabled(cachedDevice.getDevice()));
+    }
+
     /** Returns if the broadcast is on-going. */
     @WorkerThread
     public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index f18a2da..08f7806 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -54,6 +54,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
 
 import com.android.settingslib.R;
 import com.android.settingslib.flags.Flags;
@@ -64,6 +65,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.nio.charset.StandardCharsets;
 import java.security.SecureRandom;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -107,6 +109,7 @@
     private static final String SETTINGS_PKG = "com.android.settings";
     private static final String SYSUI_PKG = "com.android.systemui";
     private static final String TAG = "LocalBluetoothLeBroadcast";
+    private static final String AUTO_REJOIN_BROADCAST_TAG = "REJOIN_LE_BROADCAST_ID";
     private static final boolean DEBUG = BluetoothUtils.D;
     private static final String VALID_PASSWORD_CHARACTERS =
             "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,"
@@ -120,6 +123,7 @@
     // Order of this profile in device profiles list
     private static final int ORDINAL = 1;
     static final int UNKNOWN_VALUE_PLACEHOLDER = -1;
+    private static final int JUST_BOND_MILLIS_THRESHOLD = 30000; // 30s
     private static final Uri[] SETTINGS_URIS =
             new Uri[] {
                 Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME),
@@ -1283,4 +1287,87 @@
         UserManager userManager = context.getSystemService(UserManager.class);
         return userManager != null && userManager.isManagedProfile();
     }
+
+    /** Handle profile connected for {@link CachedBluetoothDevice}. */
+    @WorkerThread
+    public void handleProfileConnected(@NonNull CachedBluetoothDevice cachedDevice,
+            int bluetoothProfile, @Nullable LocalBluetoothManager btManager) {
+        if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()) {
+            Log.d(TAG, "Skip handleProfileConnected, flag off");
+            return;
+        }
+        if (!SYSUI_PKG.equals(mContext.getPackageName())) {
+            Log.d(TAG, "Skip handleProfileConnected, not a valid caller");
+            return;
+        }
+        if (!BluetoothUtils.isMediaDevice(cachedDevice)) {
+            Log.d(TAG, "Skip handleProfileConnected, not a media device");
+            return;
+        }
+        Timestamp bondTimestamp = cachedDevice.getBondTimestamp();
+        if (bondTimestamp != null) {
+            long diff = System.currentTimeMillis() - bondTimestamp.getTime();
+            if (diff <= JUST_BOND_MILLIS_THRESHOLD) {
+                Log.d(TAG, "Skip handleProfileConnected, just bond within " + diff);
+                return;
+            }
+        }
+        if (!isEnabled(null)) {
+            Log.d(TAG, "Skip handleProfileConnected, not broadcasting");
+            return;
+        }
+        BluetoothDevice device = cachedDevice.getDevice();
+        if (device == null) {
+            Log.d(TAG, "Skip handleProfileConnected, null device");
+            return;
+        }
+        // TODO: sync source in a reasonable place
+        if (BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(device, btManager)) {
+            Log.d(TAG, "Skip handleProfileConnected, already has source");
+            return;
+        }
+        if (isAutoRejoinDevice(device)) {
+            Log.d(TAG, "Skip handleProfileConnected, auto rejoin device");
+            return;
+        }
+        boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice);
+        // For eligible (LE audio) remote device, we only check assistant profile connected.
+        if (isLeAudioSupported
+                && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
+            Log.d(TAG, "Skip handleProfileConnected, lea sink, not the assistant profile");
+            return;
+        }
+        boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
+        // For ineligible (classic) remote device, we only check its first connected profile.
+        if (!isLeAudioSupported && !isFirstConnectedProfile) {
+            Log.d(TAG, "Skip handleProfileConnected, classic sink, not the first profile");
+            return;
+        }
+
+        Intent intent = new Intent(
+                LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED);
+        intent.putExtra(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device);
+        intent.setPackage(SETTINGS_PKG);
+        Log.d(TAG, "notify device connected, device = " + device.getAnonymizedAddress());
+
+        mContext.sendBroadcast(intent);
+    }
+
+    private boolean isAutoRejoinDevice(@Nullable BluetoothDevice bluetoothDevice) {
+        String metadataValue = BluetoothUtils.getFastPairCustomizedField(bluetoothDevice,
+                AUTO_REJOIN_BROADCAST_TAG);
+        return getLatestBroadcastId() != UNKNOWN_VALUE_PLACEHOLDER && Objects.equals(metadataValue,
+                String.valueOf(getLatestBroadcastId()));
+    }
+
+    private boolean isFirstConnectedProfile(@Nullable CachedBluetoothDevice cachedDevice,
+            int bluetoothProfile) {
+        if (cachedDevice == null) return false;
+        return cachedDevice.getProfiles().stream()
+                .noneMatch(
+                        profile ->
+                                profile.getProfileId() != bluetoothProfile
+                                        && profile.getConnectionStatus(cachedDevice.getDevice())
+                                        == BluetoothProfile.STATE_CONNECTED);
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
index ae17acb..8bb41cc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
+++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
@@ -16,8 +16,8 @@
 
 package com.android.settingslib.qrcode;
 
+import android.annotation.NonNull;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
@@ -75,12 +75,29 @@
 
     @VisibleForTesting
     Camera mCamera;
+    Camera.CameraInfo mCameraInfo;
+
+    /**
+     * The size of the preview image as requested to camera, e.g. 1920x1080.
+     */
     private Size mPreviewSize;
+
+    /**
+     * Whether the preview image would be displayed in "portrait" (width less
+     * than height) orientation in current display orientation.
+     *
+     * Note that we don't distinguish between a rotation of 90 degrees or 270
+     * degrees here, since we center crop all the preview.
+     *
+     * TODO: Handle external camera / multiple display, this likely requires
+     * migrating to newer Camera2 API.
+     */
+    private boolean mPreviewInPortrait;
+
     private WeakReference<Context> mContext;
     private ScannerCallback mScannerCallback;
     private MultiFormatReader mReader;
     private DecodingTask mDecodeTask;
-    private int mCameraOrientation;
     @VisibleForTesting
     Camera.Parameters mParameters;
 
@@ -152,8 +169,14 @@
          * @param previewSize       Is the preview size set by camera
          * @param cameraOrientation Is the orientation of current Camera
          * @return The rectangle would like to crop from the camera preview shot.
+         * @deprecated This is no longer used, and the frame position is
+         *     automatically calculated from the preview size and the
+         *     background View size.
          */
-        Rect getFramePosition(Size previewSize, int cameraOrientation);
+        @Deprecated
+        default @NonNull Rect getFramePosition(@NonNull Size previewSize, int cameraOrientation) {
+            throw new AssertionError("getFramePosition shouldn't be used");
+        }
 
         /**
          * Sets the transform to associate with preview area.
@@ -172,6 +195,41 @@
         boolean isValid(String qrCode);
     }
 
+    private boolean setPreviewDisplayOrientation() {
+        if (mContext.get() == null) {
+            return false;
+        }
+
+        final WindowManager winManager =
+                (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
+        final int rotation = winManager.getDefaultDisplay().getRotation();
+        int degrees = 0;
+        switch (rotation) {
+            case Surface.ROTATION_0:
+                degrees = 0;
+                break;
+            case Surface.ROTATION_90:
+                degrees = 90;
+                break;
+            case Surface.ROTATION_180:
+                degrees = 180;
+                break;
+            case Surface.ROTATION_270:
+                degrees = 270;
+                break;
+        }
+        int rotateDegrees = 0;
+        if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+            rotateDegrees = (mCameraInfo.orientation + degrees) % 360;
+            rotateDegrees = (360 - rotateDegrees) % 360;  // compensate the mirror
+        } else {
+            rotateDegrees = (mCameraInfo.orientation - degrees + 360) % 360;
+        }
+        mCamera.setDisplayOrientation(rotateDegrees);
+        mPreviewInPortrait = (rotateDegrees == 90 || rotateDegrees == 270);
+        return true;
+    }
+
     @VisibleForTesting
     void setCameraParameter() {
         mParameters = mCamera.getParameters();
@@ -195,37 +253,39 @@
         mCamera.setParameters(mParameters);
     }
 
-    private boolean startPreview() {
-        if (mContext.get() == null) {
-            return false;
-        }
+    /**
+     * Set transform matrix to crop and center the preview picture.
+     */
+    private void setTransformationMatrix() {
+        final Size previewDisplaySize = rotateIfPortrait(mPreviewSize);
+        final Size viewSize = mScannerCallback.getViewSize();
+        final Rect cropRegion = calculateCenteredCrop(previewDisplaySize, viewSize);
 
-        final WindowManager winManager =
-                (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
-        final int rotation = winManager.getDefaultDisplay().getRotation();
-        int degrees = 0;
-        switch (rotation) {
-            case Surface.ROTATION_0:
-                degrees = 0;
-                break;
-            case Surface.ROTATION_90:
-                degrees = 90;
-                break;
-            case Surface.ROTATION_180:
-                degrees = 180;
-                break;
-            case Surface.ROTATION_270:
-                degrees = 270;
-                break;
-        }
-        final int rotateDegrees = (mCameraOrientation - degrees + 360) % 360;
-        mCamera.setDisplayOrientation(rotateDegrees);
+        // Note that strictly speaking, since the preview is mirrored in front
+        // camera case, we should also mirror the crop region here. But since
+        // we're cropping at the center, mirroring would result in the same
+        // crop region other than small off-by-one error from floating point
+        // calculation and wouldn't be noticeable.
+
+        // Calculate transformation matrix.
+        float scaleX = previewDisplaySize.getWidth() / (float) cropRegion.width();
+        float scaleY = previewDisplaySize.getHeight() / (float) cropRegion.height();
+        float translateX = -cropRegion.left / (float) cropRegion.width() * viewSize.getWidth();
+        float translateY = -cropRegion.top / (float) cropRegion.height() * viewSize.getHeight();
+
+        // Set the transform matrix.
+        final Matrix matrix = new Matrix();
+        matrix.setScale(scaleX, scaleY);
+        matrix.postTranslate(translateX, translateY);
+        mScannerCallback.setTransform(matrix);
+    }
+
+    private void startPreview() {
         mCamera.startPreview();
         if (Camera.Parameters.FOCUS_MODE_AUTO.equals(mParameters.getFocusMode())) {
             mCamera.autoFocus(/* Camera.AutoFocusCallback */ null);
             sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS);
         }
-        return true;
     }
 
     private class DecodingTask extends AsyncTask<Void, Void, String> {
@@ -300,7 +360,7 @@
                     if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                         releaseCamera();
                         mCamera = Camera.open(i);
-                        mCameraOrientation = cameraInfo.orientation;
+                        mCameraInfo = cameraInfo;
                         break;
                     }
                 }
@@ -309,7 +369,7 @@
                     Camera.getCameraInfo(0, cameraInfo);
                     releaseCamera();
                     mCamera = Camera.open(0);
-                    mCameraOrientation = cameraInfo.orientation;
+                    mCameraInfo = cameraInfo;
                 }
             } catch (RuntimeException e) {
                 Log.e(TAG, "Fail to open camera: " + e);
@@ -323,11 +383,12 @@
                     throw new IOException("Cannot find available camera");
                 }
                 mCamera.setPreviewTexture(surface);
+                if (!setPreviewDisplayOrientation()) {
+                    throw new IOException("Lost context");
+                }
                 setCameraParameter();
                 setTransformationMatrix();
-                if (!startPreview()) {
-                    throw new IOException("Lost contex");
-                }
+                startPreview();
             } catch (IOException ioe) {
                 Log.e(TAG, "Fail to startPreview camera: " + ioe);
                 mCamera = null;
@@ -345,32 +406,30 @@
         }
     }
 
-    /** Set transform matrix to crop and center the preview picture */
-    private void setTransformationMatrix() {
-        final boolean isPortrait = mContext.get().getResources().getConfiguration().orientation
-                == Configuration.ORIENTATION_PORTRAIT;
-
-        final int previewWidth = isPortrait ? mPreviewSize.getWidth() : mPreviewSize.getHeight();
-        final int previewHeight = isPortrait ? mPreviewSize.getHeight() : mPreviewSize.getWidth();
-        final float ratioPreview = (float) getRatio(previewWidth, previewHeight);
-
-        // Calculate transformation matrix.
-        float scaleX = 1.0f;
-        float scaleY = 1.0f;
-        if (previewWidth > previewHeight) {
-            scaleY = scaleX / ratioPreview;
+    /**
+     * Calculates the crop region in `previewSize` to have the same aspect
+     * ratio as `viewSize` and center aligned.
+     */
+    private Rect calculateCenteredCrop(Size previewSize, Size viewSize) {
+        final double previewRatio = getRatio(previewSize);
+        final double viewRatio = getRatio(viewSize);
+        int width;
+        int height;
+        if (previewRatio > viewRatio) {
+            width = previewSize.getWidth();
+            height = (int) Math.round(width * viewRatio);
         } else {
-            scaleX = scaleY / ratioPreview;
+            height = previewSize.getHeight();
+            width = (int) Math.round(height / viewRatio);
         }
-
-        // Set the transform matrix.
-        final Matrix matrix = new Matrix();
-        matrix.setScale(scaleX, scaleY);
-        mScannerCallback.setTransform(matrix);
+        final int left = (previewSize.getWidth() - width) / 2;
+        final int top = (previewSize.getHeight() - height) / 2;
+        return new Rect(left, top, left + width, top + height);
     }
 
     private QrYuvLuminanceSource getFrameImage(byte[] imageData) {
-        final Rect frame = mScannerCallback.getFramePosition(mPreviewSize, mCameraOrientation);
+        final Size viewSize = mScannerCallback.getViewSize();
+        final Rect frame = calculateCenteredCrop(mPreviewSize, rotateIfPortrait(viewSize));
         final QrYuvLuminanceSource image = new QrYuvLuminanceSource(imageData,
                 mPreviewSize.getWidth(), mPreviewSize.getHeight());
         return (QrYuvLuminanceSource)
@@ -398,17 +457,18 @@
      */
     private Size getBestPreviewSize(Camera.Parameters parameters) {
         final double minRatioDiffPercent = 0.1;
-        final Size windowSize = mScannerCallback.getViewSize();
-        final double winRatio = getRatio(windowSize.getWidth(), windowSize.getHeight());
+        final Size viewSize = rotateIfPortrait(mScannerCallback.getViewSize());
+        final double viewRatio = getRatio(viewSize);
         double bestChoiceRatio = 0;
         Size bestChoice = new Size(0, 0);
         for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
-            double ratio = getRatio(size.width, size.height);
+            final Size newSize = toAndroidSize(size);
+            final double ratio = getRatio(newSize);
             if (size.height * size.width > bestChoice.getWidth() * bestChoice.getHeight()
-                    && (Math.abs(bestChoiceRatio - winRatio) / winRatio > minRatioDiffPercent
-                    || Math.abs(ratio - winRatio) / winRatio <= minRatioDiffPercent)) {
-                bestChoice = new Size(size.width, size.height);
-                bestChoiceRatio = getRatio(size.width, size.height);
+                    && (Math.abs(bestChoiceRatio - viewRatio) / viewRatio > minRatioDiffPercent
+                    || Math.abs(ratio - viewRatio) / viewRatio <= minRatioDiffPercent)) {
+                bestChoice = newSize;
+                bestChoiceRatio = ratio;
             }
         }
         return bestChoice;
@@ -419,25 +479,26 @@
      * picture size and aspect ratio to choose the best one.
      */
     private Size getBestPictureSize(Camera.Parameters parameters) {
-        final Camera.Size previewSize = parameters.getPreviewSize();
-        final double previewRatio = getRatio(previewSize.width, previewSize.height);
+        final Size previewSize = mPreviewSize;
+        final double previewRatio = getRatio(previewSize);
         List<Size> bestChoices = new ArrayList<>();
         final List<Size> similarChoices = new ArrayList<>();
 
         // Filter by ratio
-        for (Camera.Size size : parameters.getSupportedPictureSizes()) {
-            double ratio = getRatio(size.width, size.height);
+        for (Camera.Size picSize : parameters.getSupportedPictureSizes()) {
+            final Size size = toAndroidSize(picSize);
+            final double ratio = getRatio(size);
             if (ratio == previewRatio) {
-                bestChoices.add(new Size(size.width, size.height));
+                bestChoices.add(size);
             } else if (Math.abs(ratio - previewRatio) < MAX_RATIO_DIFF) {
-                similarChoices.add(new Size(size.width, size.height));
+                similarChoices.add(size);
             }
         }
 
         if (bestChoices.size() == 0 && similarChoices.size() == 0) {
             Log.d(TAG, "No proper picture size, return default picture size");
             Camera.Size defaultPictureSize = parameters.getPictureSize();
-            return new Size(defaultPictureSize.width, defaultPictureSize.height);
+            return toAndroidSize(defaultPictureSize);
         }
 
         if (bestChoices.size() == 0) {
@@ -447,7 +508,7 @@
         // Get the best by area
         int bestAreaDifference = Integer.MAX_VALUE;
         Size bestChoice = null;
-        final int previewArea = previewSize.width * previewSize.height;
+        final int previewArea = previewSize.getWidth() * previewSize.getHeight();
         for (Size size : bestChoices) {
             int areaDifference = Math.abs(size.getWidth() * size.getHeight() - previewArea);
             if (areaDifference < bestAreaDifference) {
@@ -458,8 +519,20 @@
         return bestChoice;
     }
 
-    private double getRatio(double x, double y) {
-        return (x < y) ? x / y : y / x;
+    private Size rotateIfPortrait(Size size) {
+        if (mPreviewInPortrait) {
+            return new Size(size.getHeight(), size.getWidth());
+        } else {
+            return size;
+        }
+    }
+
+    private double getRatio(Size size) {
+        return size.getHeight() / (double) size.getWidth();
+    }
+
+    private Size toAndroidSize(Camera.Size size) {
+        return new Size(size.width, size.height);
     }
 
     @VisibleForTesting
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
index b86f4b3..eac69234 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
@@ -23,7 +23,6 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -38,12 +37,14 @@
 import android.content.IntentFilter;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.telephony.TelephonyManager;
 
 import com.android.settingslib.R;
 import com.android.settingslib.flags.Flags;
 import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.utils.ThreadUtils;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -54,6 +55,8 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
 import org.robolectric.shadow.api.Shadow;
 
 import java.util.ArrayList;
@@ -61,7 +64,7 @@
 import java.util.List;
 
 @RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowBluetoothAdapter.class})
+@Config(shadows = {ShadowBluetoothAdapter.class, BluetoothEventManagerTest.ShadowThreadUtils.class})
 public class BluetoothEventManagerTest {
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -100,6 +103,8 @@
     private BluetoothUtils.ErrorListener mErrorListener;
     @Mock
     private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock
+    private UserManager mUserManager;
 
     private Context mContext;
     private Intent mIntent;
@@ -130,6 +135,7 @@
         mCachedDevice1 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1);
         mCachedDevice2 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2);
         mCachedDevice3 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3);
+        when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
         BluetoothUtils.setErrorListener(mErrorListener);
     }
 
@@ -196,6 +202,7 @@
      * callback.
      */
     @Test
+    @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
     public void dispatchProfileConnectionStateChanged_registerCallback_shouldDispatchCallback() {
         mBluetoothEventManager.registerCallback(mBluetoothCallback);
 
@@ -208,10 +215,12 @@
 
     /**
      * dispatchProfileConnectionStateChanged should not call {@link
-     * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing flag is off.
+     * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+     * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when audio sharing flag is off.
      */
     @Test
-    public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() {
+    @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+    public void dispatchProfileConnectionStateChanged_flagOff_noCallToBroadcastProfile() {
         setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */
                 true, /* workProfile= */ false);
         mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -219,16 +228,19 @@
                 BluetoothProfile.STATE_DISCONNECTED,
                 BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
 
-        verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
     }
 
     /**
      * dispatchProfileConnectionStateChanged should not call {@link
-     * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when the device does not
-     * support audio sharing.
+     * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+     * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when the device does not support
+     * audio sharing.
      */
     @Test
-    public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() {
+    @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+    public void dispatchProfileConnectionStateChanged_notSupport_noCallToBroadcastProfile() {
         setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */
                 true, /* workProfile= */ false);
         mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -236,7 +248,8 @@
                 BluetoothProfile.STATE_DISCONNECTED,
                 BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
 
-        verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
     }
 
     /**
@@ -245,6 +258,7 @@
      * not ready.
      */
     @Test
+    @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
     public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() {
         setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
                 false, /* workProfile= */ false);
@@ -253,7 +267,7 @@
                 BluetoothProfile.STATE_DISCONNECTED,
                 BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
 
-        verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
     }
 
     /**
@@ -262,6 +276,7 @@
      * other than LE_AUDIO_BROADCAST_ASSISTANT or state other than STATE_DISCONNECTED.
      */
     @Test
+    @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
     public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() {
         setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
                 true, /* workProfile= */ false);
@@ -270,16 +285,17 @@
                 BluetoothProfile.STATE_DISCONNECTED,
                 BluetoothProfile.LE_AUDIO);
 
-        verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
     }
 
     /**
      * dispatchProfileConnectionStateChanged should not call {@link
-     * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for
-     * work profile.
+     * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+     * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when triggered for work profile.
      */
     @Test
-    public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() {
+    @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+    public void dispatchProfileConnectionStateChanged_workProfile_noCallToBroadcastProfile() {
         setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
                 true, /* workProfile= */ true);
         mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -287,7 +303,8 @@
                 BluetoothProfile.STATE_DISCONNECTED,
                 BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
 
-        verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
     }
 
     /**
@@ -296,7 +313,8 @@
      * disconnected and audio sharing is enabled.
      */
     @Test
-    public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() {
+    @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+    public void dispatchProfileConnectionStateChanged_assistDisconnected_updateFallbackDevice() {
         setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
                 true, /* workProfile= */ false);
         mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -305,6 +323,27 @@
                 BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
 
         verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
+    }
+
+    /**
+     * dispatchProfileConnectionStateChanged should call {@link
+     * LocalBluetoothLeBroadcast}#handleProfileConnected when assistant profile is connected and
+     * audio sharing is enabled.
+     */
+    @Test
+    @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+    public void dispatchProfileConnectionStateChanged_assistConnected_handleStateChanged() {
+        setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
+                true, /* workProfile= */ false);
+        mBluetoothEventManager.dispatchProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+
+        verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+        verify(mBroadcast).handleProfileConnected(mCachedBluetoothDevice,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mBtManager);
     }
 
     private void setUpAudioSharing(boolean enableFlag, boolean enableFeature,
@@ -325,13 +364,19 @@
         LocalBluetoothLeBroadcastAssistant assistant =
                 mock(LocalBluetoothLeBroadcastAssistant.class);
         when(assistant.isProfileReady()).thenReturn(enableProfile);
-        LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
-        when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
-        when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
-        when(mBtManager.getProfileManager()).thenReturn(profileManager);
-        UserManager userManager = mock(UserManager.class);
-        when(mContext.getSystemService(UserManager.class)).thenReturn(userManager);
-        when(userManager.isManagedProfile()).thenReturn(workProfile);
+        when(mLocalProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mLocalProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
+        when(mUserManager.isManagedProfile()).thenReturn(workProfile);
+        if (workProfile) {
+            mBluetoothEventManager =
+                    new BluetoothEventManager(
+                            mLocalAdapter,
+                            mBtManager,
+                            mCachedDeviceManager,
+                            mContext,
+                            /* handler= */ null,
+                            /* userHandle= */ null);
+        }
     }
 
     @Test
@@ -665,4 +710,12 @@
 
         verify(mBluetoothCallback).onAutoOnStateChanged(anyInt());
     }
+
+    @Implements(value = ThreadUtils.class)
+    public static class ShadowThreadUtils {
+        @Implementation
+        protected static void postOnBackgroundThread(Runnable runnable) {
+            runnable.run();
+        }
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 0325c0e..b781412 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -1349,6 +1349,36 @@
     }
 
     @Test
+    public void isMediaDevice_returnsFalse() {
+        when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mAssistant));
+        assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isFalse();
+    }
+
+    @Test
+    public void isMediaDevice_returnsTrue() {
+        when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+        assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isTrue();
+    }
+
+    @Test
+    public void isLeAudioSupported_returnsFalse() {
+        when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+        when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(false);
+
+        assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isFalse();
+    }
+
+    @Test
+    public void isLeAudioSupported_returnsTrue() {
+        when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+        when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true);
+
+        assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isTrue();
+    }
+
+    @Test
     public void isTemporaryBondDevice_hasMetadata_returnsTrue() {
         when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
                 .thenReturn(TEMP_BOND_METADATA.getBytes());
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
index a9fd380..76b6aa8 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
@@ -28,6 +28,7 @@
 public class ShadowColorDisplayManager extends org.robolectric.shadows.ShadowColorDisplayManager {
 
     private boolean mIsReduceBrightColorsActivated;
+    private int mColorMode;
 
     @Implementation
     @SystemApi
@@ -43,4 +44,13 @@
         return mIsReduceBrightColorsActivated;
     }
 
+    @Implementation
+    public int getColorMode() {
+        return mColorMode;
+    }
+
+    @Implementation
+    public void setColorMode(int colorMode) {
+        mColorMode = colorMode;
+    }
 }
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index cc01071..1362ffe 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -24,6 +24,17 @@
       "name": "SystemUIGoogleTests"
     },
     {
+      "name": "SystemUIClocksTests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
       // Permission indicators
       "name": "CtsPermissionUiTestCases",
       "options": [
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
index 71ec63c..84370ed 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
@@ -31,6 +31,7 @@
 import com.android.compose.theme.typography.TypefaceNames
 import com.android.compose.theme.typography.TypefaceTokens
 import com.android.compose.theme.typography.TypographyTokens
+import com.android.compose.theme.typography.VariableFontTypeScaleEmphasizedTokens
 import com.android.compose.theme.typography.platformTypography
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.compose.windowsizeclass.calculateWindowSizeClass
@@ -44,9 +45,15 @@
     val colorScheme = remember(context, isDarkTheme) { platformColorScheme(isDarkTheme, context) }
     val androidColorScheme = remember(context) { AndroidColorScheme(context) }
     val typefaceNames = remember(context) { TypefaceNames.get(context) }
+    val typefaceTokens = remember(typefaceNames) { TypefaceTokens(typefaceNames) }
     val typography =
-        remember(typefaceNames) {
-            platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames))))
+        remember(typefaceTokens) {
+            platformTypography(
+                TypographyTokens(
+                    TypeScaleTokens(typefaceTokens),
+                    VariableFontTypeScaleEmphasizedTokens(typefaceTokens),
+                )
+            )
         }
     val windowSizeClass = calculateWindowSizeClass()
 
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
index 1ce1ae3..652f946 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
@@ -16,6 +16,7 @@
 
 package com.android.compose.theme.typography
 
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Typography
 
@@ -25,6 +26,7 @@
  * Do not use directly and call [MaterialTheme.typography] instead to access the different text
  * styles.
  */
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 internal fun platformTypography(typographyTokens: TypographyTokens): Typography {
     return Typography(
         displayLarge = typographyTokens.displayLarge,
@@ -42,5 +44,21 @@
         labelLarge = typographyTokens.labelLarge,
         labelMedium = typographyTokens.labelMedium,
         labelSmall = typographyTokens.labelSmall,
+        // GSF emphasized tokens
+        displayLargeEmphasized = typographyTokens.displayLargeEmphasized,
+        displayMediumEmphasized = typographyTokens.displayMediumEmphasized,
+        displaySmallEmphasized = typographyTokens.displaySmallEmphasized,
+        headlineLargeEmphasized = typographyTokens.headlineLargeEmphasized,
+        headlineMediumEmphasized = typographyTokens.headlineMediumEmphasized,
+        headlineSmallEmphasized = typographyTokens.headlineSmallEmphasized,
+        titleLargeEmphasized = typographyTokens.titleLargeEmphasized,
+        titleMediumEmphasized = typographyTokens.titleMediumEmphasized,
+        titleSmallEmphasized = typographyTokens.titleSmallEmphasized,
+        bodyLargeEmphasized = typographyTokens.bodyLargeEmphasized,
+        bodyMediumEmphasized = typographyTokens.bodyMediumEmphasized,
+        bodySmallEmphasized = typographyTokens.bodySmallEmphasized,
+        labelLargeEmphasized = typographyTokens.labelLargeEmphasized,
+        labelMediumEmphasized = typographyTokens.labelMediumEmphasized,
+        labelSmallEmphasized = typographyTokens.labelSmallEmphasized,
     )
 }
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
index 13acfd6..280b8d9 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
@@ -34,6 +34,29 @@
     private val brandFont = DeviceFontFamilyName(typefaceNames.brand)
     private val plainFont = DeviceFontFamilyName(typefaceNames.plain)
 
+    // Google Sans Flex emphasized styles
+    private val displayLargeEmphasizedFont =
+        DeviceFontFamilyName("variable-display-large-emphasized")
+    private val displayMediumEmphasizedFont =
+        DeviceFontFamilyName("variable-display-medium-emphasized")
+    private val displaySmallEmphasizedFont =
+        DeviceFontFamilyName("variable-display-small-emphasized")
+    private val headlineLargeEmphasizedFont =
+        DeviceFontFamilyName("variable-headline-large-emphasized")
+    private val headlineMediumEmphasizedFont =
+        DeviceFontFamilyName("variable-headline-medium-emphasized")
+    private val headlineSmallEmphasizedFont =
+        DeviceFontFamilyName("variable-headline-small-emphasized")
+    private val titleLargeEmphasizedFont = DeviceFontFamilyName("variable-title-large-emphasized")
+    private val titleMediumEmphasizedFont = DeviceFontFamilyName("variable-title-medium-emphasized")
+    private val titleSmallEmphasizedFont = DeviceFontFamilyName("variable-title-small-emphasized")
+    private val bodyLargeEmphasizedFont = DeviceFontFamilyName("variable-body-large-emphasized")
+    private val bodyMediumEmphasizedFont = DeviceFontFamilyName("variable-body-medium-emphasized")
+    private val bodySmallEmphasizedFont = DeviceFontFamilyName("variable-body-small-emphasized")
+    private val labelLargeEmphasizedFont = DeviceFontFamilyName("variable-label-large-emphasized")
+    private val labelMediumEmphasizedFont = DeviceFontFamilyName("variable-label-medium-emphasized")
+    private val labelSmallEmphasizedFont = DeviceFontFamilyName("variable-label-small-emphasized")
+
     val brand =
         FontFamily(
             Font(brandFont, weight = WeightMedium),
@@ -44,6 +67,22 @@
             Font(plainFont, weight = WeightMedium),
             Font(plainFont, weight = WeightRegular),
         )
+
+    val displayLargeEmphasized = FontFamily(Font(displayLargeEmphasizedFont))
+    val displayMediumEmphasized = FontFamily(Font(displayMediumEmphasizedFont))
+    val displaySmallEmphasized = FontFamily(Font(displaySmallEmphasizedFont))
+    val headlineLargeEmphasized = FontFamily(Font(headlineLargeEmphasizedFont))
+    val headlineMediumEmphasized = FontFamily(Font(headlineMediumEmphasizedFont))
+    val headlineSmallEmphasized = FontFamily(Font(headlineSmallEmphasizedFont))
+    val titleLargeEmphasized = FontFamily(Font(titleLargeEmphasizedFont))
+    val titleMediumEmphasized = FontFamily(Font(titleMediumEmphasizedFont))
+    val titleSmallEmphasized = FontFamily(Font(titleSmallEmphasizedFont))
+    val bodyLargeEmphasized = FontFamily(Font(bodyLargeEmphasizedFont))
+    val bodyMediumEmphasized = FontFamily(Font(bodyMediumEmphasizedFont))
+    val bodySmallEmphasized = FontFamily(Font(bodySmallEmphasizedFont))
+    val labelLargeEmphasized = FontFamily(Font(labelLargeEmphasizedFont))
+    val labelMediumEmphasized = FontFamily(Font(labelMediumEmphasizedFont))
+    val labelSmallEmphasized = FontFamily(Font(labelSmallEmphasizedFont))
 }
 
 internal data class TypefaceNames
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
index 38aadb8..4115647 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
@@ -18,7 +18,10 @@
 
 import androidx.compose.ui.text.TextStyle
 
-internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
+internal class TypographyTokens(
+    typeScaleTokens: TypeScaleTokens,
+    variableTypeScaleTokens: VariableFontTypeScaleEmphasizedTokens,
+) {
     val bodyLarge =
         TextStyle(
             fontFamily = typeScaleTokens.bodyLargeFont,
@@ -139,4 +142,112 @@
             lineHeight = typeScaleTokens.titleSmallLineHeight,
             letterSpacing = typeScaleTokens.titleSmallTracking,
         )
+    // GSF emphasized styles
+    // note: we don't need to define fontWeight or axes values because they are pre-defined
+    // as part of the font family in fonts_customization.xml (for performance optimization)
+    val displayLargeEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.displayLargeFont,
+            fontSize = variableTypeScaleTokens.displayLargeSize,
+            lineHeight = variableTypeScaleTokens.displayLargeLineHeight,
+            letterSpacing = variableTypeScaleTokens.displayLargeTracking,
+        )
+    val displayMediumEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.displayMediumFont,
+            fontSize = variableTypeScaleTokens.displayMediumSize,
+            lineHeight = variableTypeScaleTokens.displayMediumLineHeight,
+            letterSpacing = variableTypeScaleTokens.displayMediumTracking,
+        )
+    val displaySmallEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.displaySmallFont,
+            fontSize = variableTypeScaleTokens.displaySmallSize,
+            lineHeight = variableTypeScaleTokens.displaySmallLineHeight,
+            letterSpacing = variableTypeScaleTokens.displaySmallTracking,
+        )
+    val headlineLargeEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.headlineLargeFont,
+            fontSize = variableTypeScaleTokens.headlineLargeSize,
+            lineHeight = variableTypeScaleTokens.headlineLargeLineHeight,
+            letterSpacing = variableTypeScaleTokens.headlineLargeTracking,
+        )
+    val headlineMediumEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.headlineMediumFont,
+            fontSize = variableTypeScaleTokens.headlineMediumSize,
+            lineHeight = variableTypeScaleTokens.headlineMediumLineHeight,
+            letterSpacing = variableTypeScaleTokens.headlineMediumTracking,
+        )
+    val headlineSmallEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.headlineSmallFont,
+            fontSize = variableTypeScaleTokens.headlineSmallSize,
+            lineHeight = variableTypeScaleTokens.headlineSmallLineHeight,
+            letterSpacing = variableTypeScaleTokens.headlineSmallTracking,
+        )
+    val titleLargeEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.titleLargeFont,
+            fontSize = variableTypeScaleTokens.titleLargeSize,
+            lineHeight = variableTypeScaleTokens.titleLargeLineHeight,
+            letterSpacing = variableTypeScaleTokens.titleLargeTracking,
+        )
+    val titleMediumEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.titleMediumFont,
+            fontSize = variableTypeScaleTokens.titleMediumSize,
+            lineHeight = variableTypeScaleTokens.titleMediumLineHeight,
+            letterSpacing = variableTypeScaleTokens.titleMediumTracking,
+        )
+    val titleSmallEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.titleSmallFont,
+            fontSize = variableTypeScaleTokens.titleSmallSize,
+            lineHeight = variableTypeScaleTokens.titleSmallLineHeight,
+            letterSpacing = variableTypeScaleTokens.titleSmallTracking,
+        )
+    val bodyLargeEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.bodyLargeFont,
+            fontSize = variableTypeScaleTokens.bodyLargeSize,
+            lineHeight = variableTypeScaleTokens.bodyLargeLineHeight,
+            letterSpacing = variableTypeScaleTokens.bodyLargeTracking,
+        )
+    val bodyMediumEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.bodyMediumFont,
+            fontSize = variableTypeScaleTokens.bodyMediumSize,
+            lineHeight = variableTypeScaleTokens.bodyMediumLineHeight,
+            letterSpacing = variableTypeScaleTokens.bodyMediumTracking,
+        )
+    val bodySmallEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.bodySmallFont,
+            fontSize = variableTypeScaleTokens.bodySmallSize,
+            lineHeight = variableTypeScaleTokens.bodySmallLineHeight,
+            letterSpacing = variableTypeScaleTokens.bodySmallTracking,
+        )
+    val labelLargeEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.labelLargeFont,
+            fontSize = variableTypeScaleTokens.labelLargeSize,
+            lineHeight = variableTypeScaleTokens.labelLargeLineHeight,
+            letterSpacing = variableTypeScaleTokens.labelLargeTracking,
+        )
+    val labelMediumEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.labelMediumFont,
+            fontSize = variableTypeScaleTokens.labelMediumSize,
+            lineHeight = variableTypeScaleTokens.labelMediumLineHeight,
+            letterSpacing = variableTypeScaleTokens.labelMediumTracking,
+        )
+    val labelSmallEmphasized =
+        TextStyle(
+            fontFamily = variableTypeScaleTokens.labelSmallFont,
+            fontSize = variableTypeScaleTokens.labelSmallSize,
+            lineHeight = variableTypeScaleTokens.labelSmallLineHeight,
+            letterSpacing = variableTypeScaleTokens.labelSmallTracking,
+        )
 }
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt
new file mode 100644
index 0000000..52b9390
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.compose.theme.typography
+
+import androidx.compose.ui.unit.sp
+
+internal class VariableFontTypeScaleEmphasizedTokens(typefaceTokens: TypefaceTokens) {
+    val bodyLargeFont = typefaceTokens.bodyLargeEmphasized
+    val bodyLargeLineHeight = 24.0.sp
+    val bodyLargeSize = 16.sp
+    val bodyLargeTracking = 0.0.sp
+    val bodyMediumFont = typefaceTokens.bodyMediumEmphasized
+    val bodyMediumLineHeight = 20.0.sp
+    val bodyMediumSize = 14.sp
+    val bodyMediumTracking = 0.0.sp
+    val bodySmallFont = typefaceTokens.bodySmallEmphasized
+    val bodySmallLineHeight = 16.0.sp
+    val bodySmallSize = 12.sp
+    val bodySmallTracking = 0.0.sp
+    val displayLargeFont = typefaceTokens.displayLargeEmphasized
+    val displayLargeLineHeight = 64.0.sp
+    val displayLargeSize = 57.sp
+    val displayLargeTracking = 0.0.sp
+    val displayMediumFont = typefaceTokens.displayMediumEmphasized
+    val displayMediumLineHeight = 52.0.sp
+    val displayMediumSize = 45.sp
+    val displayMediumTracking = 0.0.sp
+    val displaySmallFont = typefaceTokens.displaySmallEmphasized
+    val displaySmallLineHeight = 44.0.sp
+    val displaySmallSize = 36.sp
+    val displaySmallTracking = 0.0.sp
+    val headlineLargeFont = typefaceTokens.headlineLargeEmphasized
+    val headlineLargeLineHeight = 40.0.sp
+    val headlineLargeSize = 32.sp
+    val headlineLargeTracking = 0.0.sp
+    val headlineMediumFont = typefaceTokens.headlineMediumEmphasized
+    val headlineMediumLineHeight = 36.0.sp
+    val headlineMediumSize = 28.sp
+    val headlineMediumTracking = 0.0.sp
+    val headlineSmallFont = typefaceTokens.headlineSmallEmphasized
+    val headlineSmallLineHeight = 32.0.sp
+    val headlineSmallSize = 24.sp
+    val headlineSmallTracking = 0.0.sp
+    val labelLargeFont = typefaceTokens.labelLargeEmphasized
+    val labelLargeLineHeight = 20.0.sp
+    val labelLargeSize = 14.sp
+    val labelLargeTracking = 0.0.sp
+    val labelMediumFont = typefaceTokens.labelMediumEmphasized
+    val labelMediumLineHeight = 16.0.sp
+    val labelMediumSize = 12.sp
+    val labelMediumTracking = 0.0.sp
+    val labelSmallFont = typefaceTokens.labelSmallEmphasized
+    val labelSmallLineHeight = 16.0.sp
+    val labelSmallSize = 11.sp
+    val labelSmallTracking = 0.0.sp
+    val titleLargeFont = typefaceTokens.titleLargeEmphasized
+    val titleLargeLineHeight = 28.0.sp
+    val titleLargeSize = 22.sp
+    val titleLargeTracking = 0.0.sp
+    val titleMediumFont = typefaceTokens.titleMediumEmphasized
+    val titleMediumLineHeight = 24.0.sp
+    val titleMediumSize = 16.sp
+    val titleMediumTracking = 0.0.sp
+    val titleSmallFont = typefaceTokens.titleSmallEmphasized
+    val titleSmallLineHeight = 20.0.sp
+    val titleSmallSize = 14.sp
+    val titleSmallTracking = 0.0.sp
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
index 48dee24..f1b273a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
 import com.android.compose.animation.scene.ContentScope
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.UserAction
@@ -102,6 +103,8 @@
             viewModel,
             dialogFactory,
             Modifier.element(Bouncer.Elements.Content)
+                // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+                .testTag(Bouncer.Elements.Content.testTag)
                 .overscroll(verticalOverscrollEffect)
                 .sysuiResTag(Bouncer.TestTags.Root)
                 .fillMaxSize(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 5e61af6..aa07370 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
 import com.android.compose.animation.scene.ContentScope
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
@@ -55,7 +56,11 @@
 
     @Composable
     override fun ContentScope.Content(modifier: Modifier) {
-        LockscreenScene(lockscreenContent = lockscreenContent, modifier = modifier)
+        LockscreenScene(
+            lockscreenContent = lockscreenContent,
+            // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+            modifier = modifier.testTag(key.rootElementKey.testTag),
+        )
     }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index b11c83c..4b3ebc2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -35,9 +35,9 @@
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon as MaterialIcon
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Slider
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -73,11 +73,15 @@
 import com.android.systemui.res.R
 import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
+import com.android.systemui.volume.ui.slider.AccessibilityParams
+import com.android.systemui.volume.ui.slider.Haptics
+import com.android.systemui.volume.ui.slider.Slider
 import kotlin.math.round
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun VolumeSlider(
     state: SliderState,
@@ -102,17 +106,6 @@
         return
     }
 
-    val value by valueState(state)
-    val interactionSource = remember { MutableInteractionSource() }
-    val hapticsViewModel: SliderHapticsViewModel? =
-        setUpHapticsViewModel(
-            value,
-            state.valueRange,
-            state.hapticFilter,
-            interactionSource,
-            hapticsViewModelFactory,
-        )
-
     Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) {
         Row(
             horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -134,60 +127,30 @@
             )
             button?.invoke()
         }
+
         Slider(
-            value = value,
+            value = state.value,
             valueRange = state.valueRange,
-            onValueChange = { newValue ->
-                hapticsViewModel?.addVelocityDataPoint(newValue)
-                onValueChange(newValue)
-            },
-            onValueChangeFinished = {
-                hapticsViewModel?.onValueChangeEnded()
-                onValueChangeFinished?.invoke()
-            },
-            enabled = state.isEnabled,
+            onValueChanged = onValueChange,
+            onValueChangeFinished = { onValueChangeFinished?.invoke() },
+            isEnabled = state.isEnabled,
+            stepDistance = state.a11yStep,
+            accessibilityParams =
+                AccessibilityParams(
+                    label = state.label,
+                    disabledMessage = state.disabledMessage,
+                    currentStateDescription = state.a11yStateDescription,
+                ),
+            haptics =
+                hapticsViewModelFactory?.let {
+                    Haptics.Enabled(
+                        hapticsViewModelFactory = it,
+                        hapticFilter = state.hapticFilter,
+                        orientation = Orientation.Horizontal,
+                    )
+                } ?: Haptics.Disabled,
             modifier =
-                Modifier.height(40.dp)
-                    .padding(top = 4.dp, bottom = 12.dp)
-                    .sysuiResTag(state.label)
-                    .clearAndSetSemantics {
-                        if (state.isEnabled) {
-                            contentDescription = state.label
-                            state.a11yClickDescription?.let {
-                                customActions =
-                                    listOf(
-                                        CustomAccessibilityAction(it) {
-                                            onIconTapped()
-                                            true
-                                        }
-                                    )
-                            }
-
-                            state.a11yStateDescription?.let { stateDescription = it }
-                            progressBarRangeInfo =
-                                ProgressBarRangeInfo(state.value, state.valueRange)
-                        } else {
-                            disabled()
-                            contentDescription =
-                                state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
-                        }
-                        setProgress { targetValue ->
-                            val targetDirection =
-                                when {
-                                    targetValue > value -> 1
-                                    targetValue < value -> -1
-                                    else -> 0
-                                }
-
-                            val newValue =
-                                (value + targetDirection * state.a11yStep).coerceIn(
-                                    state.valueRange.start,
-                                    state.valueRange.endInclusive,
-                                )
-                            onValueChange(newValue)
-                            true
-                        }
-                    },
+                Modifier.height(40.dp).padding(top = 4.dp, bottom = 12.dp).sysuiResTag(state.label),
         )
         state.disabledMessage?.let { disabledMessage ->
             AnimatedVisibility(visible = !state.isEnabled) {
@@ -348,7 +311,7 @@
 }
 
 @Composable
-fun setUpHapticsViewModel(
+private fun setUpHapticsViewModel(
     value: Float,
     valueRange: ClosedFloatingPointRange<Float>,
     hapticFilter: SliderHapticFeedbackFilter,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 907b5bc..05958a2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -169,7 +169,7 @@
             Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
         }
         .then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
-        .testTag(key.testTag)
+        .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
 }
 
 /**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 53d0ee1..404f1b2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -66,6 +66,8 @@
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
     swipeDetector: SwipeDetector = DefaultSwipeDetector,
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
+    // TODO(b/240432457) Remove this once test utils can access the internal STLForTesting().
+    implicitTestTags: Boolean = false,
     builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit,
 ) {
     SceneTransitionLayoutForTesting(
@@ -74,6 +76,7 @@
         swipeSourceDetector,
         swipeDetector,
         transitionInterceptionThreshold,
+        implicitTestTags = implicitTestTags,
         onLayoutImpl = null,
         builder = builder,
     )
@@ -727,10 +730,8 @@
 }
 
 /**
- * An internal version of [SceneTransitionLayout] to be used for tests.
- *
- * Important: You should use this only in tests and if you need to access the underlying
- * [SceneTransitionLayoutImpl]. In other cases, you should use [SceneTransitionLayout].
+ * An internal version of [SceneTransitionLayout] to be used for tests, that provides access to the
+ * internal [SceneTransitionLayoutImpl] and implicitly tags all scenes and elements.
  */
 @Composable
 internal fun SceneTransitionLayoutForTesting(
@@ -743,6 +744,7 @@
     sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() },
     ancestors: List<Ancestor> = remember { emptyList() },
     lookaheadScope: LookaheadScope? = null,
+    implicitTestTags: Boolean = true,
     builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
 ) {
     val density = LocalDensity.current
@@ -767,6 +769,7 @@
                 directionChangeSlop = directionChangeSlop,
                 defaultEffectFactory = defaultEffectFactory,
                 decayAnimationSpec = decayAnimationSpec,
+                implicitTestTags = implicitTestTags,
             )
             .also { onLayoutImpl?.invoke(it) }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 53996d2..e3c4eb0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -122,6 +122,9 @@
      * This is used to enable transformations and shared elements across NestedSTLs.
      */
     internal val ancestors: List<Ancestor> = emptyList(),
+
+    /** Whether elements and scene should be tagged using `Modifier.testTag`. */
+    internal val implicitTestTags: Boolean = false,
     lookaheadScope: LookaheadScope? = null,
     defaultEffectFactory: OverscrollFactory,
 ) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 9ca45fe..149a9e7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -173,7 +173,7 @@
                 .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) {
                     Modifier.container(containerState)
                 }
-                .testTag(key.testTag)
+                .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
         ) {
             CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
                 scope.content()
@@ -301,6 +301,7 @@
             sharedElementMap = layoutImpl.elements,
             ancestors = ancestors,
             lookaheadScope = layoutImpl.lookaheadScope,
+            implicitTestTags = layoutImpl.implicitTestTags,
         )
     }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 338fb9b..86cbfe4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -227,7 +227,7 @@
             to = SceneB,
             transitionLayout = { state ->
                 coroutineScope = rememberCoroutineScope()
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneA) {
                         Box(Modifier.size(layoutSize)) {
                             // Transformed element
@@ -633,7 +633,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) }
                     scene(SceneB) {}
                 }
@@ -674,7 +674,7 @@
             CompositionLocalProvider(
                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
             ) {
-                SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
                     scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                         Spacer(Modifier.fillMaxSize())
                     }
@@ -734,7 +734,7 @@
             CompositionLocalProvider(
                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
             ) {
-                SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
                     scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                         Spacer(
                             Modifier.overscroll(verticalOverscrollEffect)
@@ -834,7 +834,7 @@
             CompositionLocalProvider(
                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
             ) {
-                SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
                     scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                         Spacer(Modifier.fillMaxSize())
                     }
@@ -893,7 +893,7 @@
             CompositionLocalProvider(
                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
             ) {
-                SceneTransitionLayout(
+                SceneTransitionLayoutForTesting(
                     state = state,
                     modifier = Modifier.size(layoutWidth, layoutHeight),
                 ) {
@@ -970,7 +970,7 @@
 
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            SceneTransitionLayout(
+            SceneTransitionLayoutForTesting(
                 state = state,
                 modifier = Modifier.size(layoutWidth, layoutHeight),
             ) {
@@ -1057,7 +1057,7 @@
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
 
-            SceneTransitionLayout(state) {
+            SceneTransitionLayoutForTesting(state) {
                 scene(SceneA) {
                     Box(Modifier.size(layoutSize)) {
                         Box(
@@ -1374,7 +1374,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
                     scene(SceneA) {
                         Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) }
                     }
@@ -1742,7 +1742,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state, Modifier.size(200.dp)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                     scene(SceneA) { Foo(offset = 0.dp) }
                     scene(SceneB) { Foo(offset = 20.dp) }
                     scene(SceneC) { Foo(offset = 40.dp) }
@@ -1828,7 +1828,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
 
                     // Define A after B so that Foo is placed in A during A <=> B.
@@ -1887,7 +1887,7 @@
 
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneA) { Foo() }
                     scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
                 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index 04c762f..98ecb64 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -90,7 +90,7 @@
         lateinit var coroutineScope: CoroutineScope
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
                 overlay(OverlayA) { Foo() }
             }
@@ -132,7 +132,7 @@
         lateinit var coroutineScope: CoroutineScope
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
                 overlay(OverlayA) { Foo() }
                 overlay(OverlayB) { Foo() }
@@ -230,7 +230,7 @@
         lateinit var coroutineScope: CoroutineScope
         rule.setContent {
             coroutineScope = rememberCoroutineScope()
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) { MovableBar() } }
                 overlay(OverlayA) { MovableBar() }
                 overlay(OverlayB) { MovableBar() }
@@ -302,7 +302,7 @@
             }
         var alignment by mutableStateOf(Alignment.Center)
         rule.setContent {
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
                 overlay(OverlayA, alignment = alignment) { Foo() }
             }
@@ -761,7 +761,7 @@
         val movableElementChildTag = "movableElementChildTag"
         val scope =
             rule.setContentAndCreateMainScope {
-                SceneTransitionLayout(state) {
+                SceneTransitionLayoutForTesting(state) {
                     scene(SceneA) {
                         MovableElement(key, Modifier) {
                             content { Box(Modifier.testTag(movableElementChildTag).size(100.dp)) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
index 2bf2358..366b11d 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -250,7 +250,7 @@
             }
 
         rule.setContent {
-            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
                 scene(SceneA) { Box(Modifier.fillMaxSize()) }
                 overlay(OverlayA) { Box(Modifier.fillMaxSize()) }
                 overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index d7f7a51..fa7661b6 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -97,7 +97,7 @@
             MutableSceneTransitionLayoutStateForTests(SceneA, EmptyTestTransitions)
         }
 
-        SceneTransitionLayout(state = layoutState, modifier = Modifier.size(LayoutSize)) {
+        SceneTransitionLayoutForTesting(state = layoutState, modifier = Modifier.size(LayoutSize)) {
             scene(SceneA, userActions = mapOf(Back to SceneB)) {
                 Box(Modifier.fillMaxSize()) {
                     SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 751b314..11abbbe 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -763,7 +763,7 @@
         var touchSlop = 0f
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+            SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
                 scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
                     Box(Modifier.fillMaxSize())
                 }
@@ -837,7 +837,7 @@
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
             CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+                SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
                     scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
                         Box(Modifier.fillMaxSize())
                     }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
index bb511bc..8b56892 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
@@ -40,7 +40,7 @@
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateForTests
 import com.android.compose.animation.scene.Scale
 import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.SceneTransitionLayoutForTesting
 import com.android.compose.animation.scene.SceneTransitions
 import com.android.compose.animation.scene.TestScenes
 import com.android.compose.animation.scene.testNestedTransition
@@ -114,7 +114,7 @@
         @Composable
         (states: List<MutableSceneTransitionLayoutState>) -> Unit =
         { states ->
-            SceneTransitionLayout(states[0]) {
+            SceneTransitionLayoutForTesting(states[0]) {
                 scene(TestScenes.SceneA, content = { TestElement(elementVariant0A) })
                 scene(
                     TestScenes.SceneB,
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index 6d47bab..e56d1be 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -30,5 +30,7 @@
     content: @Composable ContentScope.() -> Unit,
 ) {
     val state = rememberMutableSceneTransitionLayoutState(currentScene)
-    SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) }
+    SceneTransitionLayout(state, modifier, implicitTestTags = true) {
+        scene(currentScene, content = content)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index f94a7ed..a362a37 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -137,7 +137,7 @@
         },
         changeState = changeState,
         transitionLayout = { state ->
-            SceneTransitionLayout(state, layoutModifier) {
+            SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
                 scene(fromScene, content = fromSceneContent)
                 scene(toScene, content = toSceneContent)
             }
@@ -163,7 +163,7 @@
             )
         },
         transitionLayout = { state ->
-            SceneTransitionLayout(state) {
+            SceneTransitionLayout(state, implicitTestTags = true) {
                 scene(fromScene) { fromSceneContent() }
                 overlay(overlay) { overlayContent() }
             }
@@ -191,7 +191,7 @@
             )
         },
         transitionLayout = { state ->
-            SceneTransitionLayout(state) {
+            SceneTransitionLayout(state, implicitTestTags = true) {
                 scene(toScene) { toSceneContent() }
                 overlay(overlay) { overlayContent() }
             }
@@ -223,7 +223,7 @@
             )
         },
         transitionLayout = { state ->
-            SceneTransitionLayout(state) {
+            SceneTransitionLayout(state, implicitTestTags = true) {
                 scene(currentScene) { currentSceneContent() }
                 overlay(from, alignment = fromAlignment) { fromContent() }
                 overlay(to, alignment = toAlignment) { toContent() }
@@ -273,7 +273,7 @@
                 }
             }
 
-            SceneTransitionLayout(state, layoutModifier) {
+            SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
                 scene(fromScene, content = fromSceneContent)
                 scene(toScene, content = toSceneContent)
             }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index aad1276..654478a 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.plugins.clocks.ClockPickerConfig
 import com.android.systemui.plugins.clocks.ClockProvider
 import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.FlexClockController.Companion.AXIS_PRESETS
 import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes
 
 private val TAG = DefaultClockProvider::class.simpleName
@@ -98,16 +99,16 @@
             throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
         }
 
-        val fontAxes =
-            if (!isClockReactiveVariantsEnabled) listOf()
-            else getDefaultAxes(settings).merge(settings.axes)
         return ClockPickerConfig(
             settings.clockId ?: DEFAULT_CLOCK_ID,
             resources.getString(R.string.clock_default_name),
             resources.getString(R.string.clock_default_description),
             resources.getDrawable(R.drawable.clock_default_thumbnail, null),
             isReactiveToTone = true,
-            axes = fontAxes,
+            axes =
+                if (!isClockReactiveVariantsEnabled) emptyList()
+                else getDefaultAxes(settings).merge(settings.axes),
+            axisPresets = if (!isClockReactiveVariantsEnabled) emptyList() else AXIS_PRESETS,
         )
     }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index ac1c5a8..1a1033b 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -132,7 +132,7 @@
             listOf(
                 GSFAxes.WEIGHT.toClockAxis(
                     type = AxisType.Float,
-                    currentValue = 400f,
+                    currentValue = 475f,
                     name = "Weight",
                     description = "Glyph Weight",
                 ),
@@ -161,5 +161,59 @@
                 GSFAxes.ROUND.toClockAxisSetting(100f),
                 GSFAxes.SLANT.toClockAxisSetting(0f),
             )
+
+        val AXIS_PRESETS =
+            listOf(
+                FONT_AXES.map { it.toSetting() },
+                LEGACY_FLEX_SETTINGS,
+                listOf( // Porcelain
+                    GSFAxes.WEIGHT.toClockAxisSetting(500f),
+                    GSFAxes.WIDTH.toClockAxisSetting(100f),
+                    GSFAxes.ROUND.toClockAxisSetting(0f),
+                    GSFAxes.SLANT.toClockAxisSetting(0f),
+                ),
+                listOf( // Midnight
+                    GSFAxes.WEIGHT.toClockAxisSetting(300f),
+                    GSFAxes.WIDTH.toClockAxisSetting(100f),
+                    GSFAxes.ROUND.toClockAxisSetting(100f),
+                    GSFAxes.SLANT.toClockAxisSetting(-10f),
+                ),
+                listOf( // Sterling
+                    GSFAxes.WEIGHT.toClockAxisSetting(1000f),
+                    GSFAxes.WIDTH.toClockAxisSetting(100f),
+                    GSFAxes.ROUND.toClockAxisSetting(0f),
+                    GSFAxes.SLANT.toClockAxisSetting(0f),
+                ),
+                listOf( // Smoky Green
+                    GSFAxes.WEIGHT.toClockAxisSetting(150f),
+                    GSFAxes.WIDTH.toClockAxisSetting(50f),
+                    GSFAxes.ROUND.toClockAxisSetting(0f),
+                    GSFAxes.SLANT.toClockAxisSetting(0f),
+                ),
+                listOf( // Iris
+                    GSFAxes.WEIGHT.toClockAxisSetting(500f),
+                    GSFAxes.WIDTH.toClockAxisSetting(100f),
+                    GSFAxes.ROUND.toClockAxisSetting(100f),
+                    GSFAxes.SLANT.toClockAxisSetting(0f),
+                ),
+                listOf( // Margarita
+                    GSFAxes.WEIGHT.toClockAxisSetting(300f),
+                    GSFAxes.WIDTH.toClockAxisSetting(30f),
+                    GSFAxes.ROUND.toClockAxisSetting(100f),
+                    GSFAxes.SLANT.toClockAxisSetting(-10f),
+                ),
+                listOf( // Raspberry
+                    GSFAxes.WEIGHT.toClockAxisSetting(700f),
+                    GSFAxes.WIDTH.toClockAxisSetting(140f),
+                    GSFAxes.ROUND.toClockAxisSetting(100f),
+                    GSFAxes.SLANT.toClockAxisSetting(-7f),
+                ),
+                listOf( // Ultra Blue
+                    GSFAxes.WEIGHT.toClockAxisSetting(850f),
+                    GSFAxes.WIDTH.toClockAxisSetting(130f),
+                    GSFAxes.ROUND.toClockAxisSetting(0f),
+                    GSFAxes.SLANT.toClockAxisSetting(0f),
+                ),
+            )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
index 781e416..ede29d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
@@ -26,6 +26,9 @@
 import com.android.systemui.common.shared.model.PackageInstallSession
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.testKosmos
@@ -173,6 +176,58 @@
         }
 
     @Test
+    fun onCreateUpdatedSession_ignoreNullPackageNameSessions() =
+        kosmos.runTest {
+            val nullPackageSession =
+                SessionInfo().apply {
+                    sessionId = 1
+                    appPackageName = null
+                    appIcon = icon1
+                }
+
+            val wellFormedSession =
+                SessionInfo().apply {
+                    sessionId = 2
+                    appPackageName = "pkg_name"
+                    appIcon = icon2
+                }
+
+            defaultSessions = listOf(wellFormedSession)
+
+            whenever(packageInstaller.allSessions).thenReturn(defaultSessions)
+            whenever(packageInstaller.getSessionInfo(1)).thenReturn(nullPackageSession)
+            whenever(packageInstaller.getSessionInfo(2)).thenReturn(wellFormedSession)
+
+            val packageInstallerMonitor =
+                PackageInstallerMonitor(
+                    handler,
+                    backgroundScope,
+                    logcatLogBuffer("PackageInstallerRepositoryImplTest"),
+                    packageInstaller,
+                )
+
+            val sessions by collectLastValue(packageInstallerMonitor.installSessionsForPrimaryUser)
+
+            // Verify flow updated with the new session
+            assertThat(sessions)
+                .comparingElementsUsing(represents)
+                .containsExactlyElementsIn(defaultSessions)
+
+            val callback =
+                withArgCaptor<PackageInstaller.SessionCallback> {
+                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+                }
+
+            // New session added
+            callback.onCreated(nullPackageSession.sessionId)
+
+            // Verify flow updated with the new session
+            assertThat(sessions)
+                .comparingElementsUsing(represents)
+                .containsExactlyElementsIn(defaultSessions)
+        }
+
+    @Test
     fun installSessions_newSessionsAreAdded() =
         testScope.runTest {
             val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
index e53155d..ed73d89 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
@@ -21,6 +21,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalSmartspaceRepository
 import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
@@ -28,12 +30,12 @@
 import com.android.systemui.communal.domain.interactor.setCommunalEnabled
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -42,46 +44,64 @@
 @EnableFlags(FLAG_COMMUNAL_HUB)
 @RunWith(AndroidJUnit4::class)
 class CommunalOngoingContentStartableTest : SysuiTestCase() {
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
 
-    private val mediaRepository = kosmos.fakeCommunalMediaRepository
-    private val smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
+    private var showUmoOnHub = true
 
-    private lateinit var underTest: CommunalOngoingContentStartable
+    private val Kosmos.underTest by
+        Kosmos.Fixture {
+            CommunalOngoingContentStartable(
+                bgScope = applicationCoroutineScope,
+                communalInteractor = communalInteractor,
+                communalMediaRepository = communalMediaRepository,
+                communalSettingsInteractor = communalSettingsInteractor,
+                communalSmartspaceRepository = communalSmartspaceRepository,
+                showUmoOnHub = showUmoOnHub,
+            )
+        }
 
     @Before
     fun setUp() {
         kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
-        underTest =
-            CommunalOngoingContentStartable(
-                bgScope = kosmos.applicationCoroutineScope,
-                communalInteractor = kosmos.communalInteractor,
-                communalMediaRepository = mediaRepository,
-                communalSettingsInteractor = kosmos.communalSettingsInteractor,
-                communalSmartspaceRepository = smartspaceRepository,
-            )
     }
 
     @Test
-    fun testListenForOngoingContentWhenCommunalIsEnabled() =
-        testScope.runTest {
+    fun testListenForOngoingContent() =
+        kosmos.runTest {
             underTest.start()
-            runCurrent()
 
-            assertThat(mediaRepository.isListening()).isFalse()
-            assertThat(smartspaceRepository.isListening()).isFalse()
+            assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+            assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
 
             kosmos.setCommunalEnabled(true)
-            runCurrent()
 
-            assertThat(mediaRepository.isListening()).isTrue()
-            assertThat(smartspaceRepository.isListening()).isTrue()
+            assertThat(fakeCommunalMediaRepository.isListening()).isTrue()
+            assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
 
             kosmos.setCommunalEnabled(false)
-            runCurrent()
 
-            assertThat(mediaRepository.isListening()).isFalse()
-            assertThat(smartspaceRepository.isListening()).isFalse()
+            assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+            assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
+        }
+
+    @Test
+    fun testListenForOngoingContent_showUmoFalse() =
+        kosmos.runTest {
+            showUmoOnHub = false
+            underTest.start()
+
+            assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+            assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
+
+            kosmos.setCommunalEnabled(true)
+
+            // Media listening does not start when UMO is disabled.
+            assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+            assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
+
+            kosmos.setCommunalEnabled(false)
+
+            assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+            assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
index 943ada9..4e14fec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
@@ -18,9 +18,6 @@
 
 import android.animation.Animator
 import android.animation.ObjectAnimator
-import android.icu.text.MeasureFormat
-import android.icu.util.Measure
-import android.icu.util.MeasureUnit
 import android.testing.TestableLooper
 import android.view.View
 import android.widget.SeekBar
@@ -33,7 +30,6 @@
 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
 import com.android.systemui.res.R
 import com.google.common.truth.Truth.assertThat
-import java.util.Locale
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -65,11 +61,11 @@
     fun setUp() {
         context.orCreateTestableResources.addOverride(
             R.dimen.qs_media_enabled_seekbar_height,
-            enabledHeight,
+            enabledHeight
         )
         context.orCreateTestableResources.addOverride(
             R.dimen.qs_media_disabled_seekbar_height,
-            disabledHeight,
+            disabledHeight
         )
 
         seekBarView = SeekBar(context)
@@ -114,31 +110,14 @@
 
     @Test
     fun seekBarProgress() {
-        val elapsedTime = 3000
-        val duration = (1.5 * 60 * 60 * 1000).toInt()
         // WHEN part of the track has been played
-        val data = SeekBarViewModel.Progress(true, true, true, false, elapsedTime, duration, true)
+        val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true)
         observer.onChanged(data)
         // THEN seek bar shows the progress
-        assertThat(seekBarView.progress).isEqualTo(elapsedTime)
-        assertThat(seekBarView.max).isEqualTo(duration)
+        assertThat(seekBarView.progress).isEqualTo(3000)
+        assertThat(seekBarView.max).isEqualTo(120000)
 
-        val expectedProgress =
-            MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
-                .formatMeasures(Measure(3, MeasureUnit.SECOND))
-        val expectedDuration =
-            MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
-                .formatMeasures(
-                    Measure(1, MeasureUnit.HOUR),
-                    Measure(30, MeasureUnit.MINUTE),
-                    Measure(0, MeasureUnit.SECOND),
-                )
-        val desc =
-            context.getString(
-                R.string.controls_media_seekbar_description,
-                expectedProgress,
-                expectedDuration,
-            )
+        val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
         assertThat(seekBarView.contentDescription).isEqualTo(desc)
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index 917f356..80ce43d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -65,8 +65,7 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
-
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
index fba6151..da3cebd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.FakeTileDetailsViewModel
 import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
 import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
 import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
@@ -97,6 +98,7 @@
                 testCoroutineDispatcher,
                 testCoroutineDispatcher,
                 testScope.backgroundScope,
+                FakeTileDetailsViewModel("QSTileViewModelImplTest"),
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
index 3db5efc..261e3de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -26,8 +26,6 @@
 import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
 import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
 import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
-import com.android.systemui.qs.tiles.dialog.InternetDetailsContentManager
-import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
 import com.android.systemui.statusbar.connectivity.AccessPointController
@@ -39,11 +37,8 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.eq
-import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
 
 @SmallTest
 @EnabledOnRavenwood
@@ -56,31 +51,17 @@
 
     private lateinit var internetDialogManager: InternetDialogManager
     private lateinit var controller: AccessPointController
-    private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
-    private lateinit var internetDetailsContentManagerFactory: InternetDetailsContentManager.Factory
-    private lateinit var internetDetailsViewModel: InternetDetailsViewModel
 
     @Before
     fun setup() {
         internetDialogManager = mock<InternetDialogManager>()
         controller = mock<AccessPointController>()
-        internetDetailsViewModelFactory = mock<InternetDetailsViewModel.Factory>()
-        internetDetailsContentManagerFactory = mock<InternetDetailsContentManager.Factory>()
-        internetDetailsViewModel =
-            InternetDetailsViewModel(
-                onLongClick = {},
-                accessPointController = mock<AccessPointController>(),
-                contentManagerFactory = internetDetailsContentManagerFactory,
-            )
-        whenever(internetDetailsViewModelFactory.create(any())).thenReturn(internetDetailsViewModel)
-
         underTest =
             InternetTileUserActionInteractor(
                 kosmos.testScope.coroutineContext,
                 internetDialogManager,
                 controller,
                 inputHandler,
-                internetDetailsViewModelFactory,
             )
     }
 
@@ -127,12 +108,4 @@
                 assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
             }
         }
-
-    @Test
-    fun detailsViewModel() =
-        kosmos.testScope.runTest {
-            assertThat(underTest.detailsViewModel.getTitle()).isEqualTo("Internet")
-            assertThat(underTest.detailsViewModel.getSubTitle())
-                .isEqualTo("Tab a network to connect")
-        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
index 0598a8b..4e9b635 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.FakeTileDetailsViewModel
 import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
@@ -171,21 +172,6 @@
                 .isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER))
         }
 
-    @Test
-    fun tileDetails() =
-        testScope.runTest {
-            assertThat(tileUserActionInteractor.detailsViewModel).isNotNull()
-            assertThat(tileUserActionInteractor.detailsViewModel?.getTitle())
-                .isEqualTo("FakeQSTileUserActionInteractor")
-            assertThat(underTest.detailsViewModel).isNotNull()
-            assertThat(underTest.detailsViewModel?.getTitle())
-                .isEqualTo("FakeQSTileUserActionInteractor")
-
-            tileUserActionInteractor.detailsViewModel = null
-            assertThat(tileUserActionInteractor.detailsViewModel).isNull()
-            assertThat(underTest.detailsViewModel).isNull()
-        }
-
     private fun createViewModel(
         scope: TestScope,
         config: QSTileConfig = tileConfig,
@@ -209,6 +195,7 @@
             testCoroutineDispatcher,
             testCoroutineDispatcher,
             scope.backgroundScope,
+            FakeTileDetailsViewModel("QSTileViewModelTest"),
         )
 
     private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
index ece21e1..166e950 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.FakeTileDetailsViewModel
 import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
@@ -253,5 +254,6 @@
             testCoroutineDispatcher,
             testCoroutineDispatcher,
             scope.backgroundScope,
+            FakeTileDetailsViewModel("QSTileViewModelUserInputTest"),
         )
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
index c69ebab..baf0aeb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
@@ -61,8 +61,7 @@
             usingMediaInComposeFragment = false // This is not for the compose fragment
         }
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
-
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 559e363..d3f5923 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -73,9 +73,8 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val fakeSceneDataSource = kosmos.fakeSceneDataSource
-
-    private val underTest = kosmos.sceneInteractor
+    private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
+    private val underTest by lazy { kosmos.sceneInteractor }
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index 4a011c0..ccc876c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -50,11 +50,10 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val configurationRepository = kosmos.fakeConfigurationRepository
-    private val keyguardRepository = kosmos.fakeKeyguardRepository
-    private val sceneInteractor = kosmos.sceneInteractor
+    private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
+    private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
-
     private val underTest by lazy { kosmos.shadeInteractorSceneContainerImpl }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index 37b4688..a832f48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -15,7 +15,9 @@
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.runCurrent
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -51,12 +53,11 @@
 @RunWith(AndroidJUnit4::class)
 @EnableSceneContainer
 class ShadeHeaderViewModelTest : SysuiTestCase() {
-    private val kosmos = testKosmos()
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
-    private val mobileIconsInteractor = kosmos.fakeMobileIconsInteractor
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val deviceEntryInteractor = kosmos.deviceEntryInteractor
-
+    private val mobileIconsInteractor by lazy { kosmos.fakeMobileIconsInteractor }
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
     private val underTest by lazy { kosmos.shadeHeaderViewModel }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 3d8da61..70df82d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -22,6 +22,7 @@
 import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_VIA_SYSUI_CALLBACKS;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -556,9 +557,9 @@
     @Test
     public void testImmersiveModeChanged() {
         final int displayAreaId = 10;
-        mCommandQueue.immersiveModeChanged(displayAreaId, true);
+        mCommandQueue.immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION);
         waitForIdleSync();
-        verify(mCallbacks).immersiveModeChanged(displayAreaId, true);
+        verify(mCallbacks).immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION);
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
index 83e26c4..5d8b68e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
@@ -65,8 +65,8 @@
 
     @Test
     @EnableFlags(NotificationBundleUi.FLAG_NAME)
-    fun getGroupRoot_adapter() {
-        assertThat(underTest.entryAdapter.groupRoot).isEqualTo(underTest.entryAdapter)
+    fun isGroupRoot_adapter() {
+        assertThat(underTest.entryAdapter.isGroupRoot).isTrue()
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
index 1f5c672..34dff24 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -542,7 +542,7 @@
 
     @Test
     @EnableFlags(NotificationBundleUi.FLAG_NAME)
-    public void getGroupRoot_adapter_groupSummary() {
+    public void isGroupRoot_adapter_groupSummary() {
         ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
         Notification notification = new Notification.Builder(mContext, "")
                 .setSmallIcon(R.drawable.ic_person)
@@ -562,12 +562,12 @@
                 .build();
         entry.setRow(row);
 
-        assertThat(entry.getEntryAdapter().getGroupRoot()).isNull();
+        assertThat(entry.getEntryAdapter().isGroupRoot()).isFalse();
     }
 
     @Test
     @EnableFlags(NotificationBundleUi.FLAG_NAME)
-    public void getGroupRoot_adapter_groupChild() {
+    public void isGroupRoot_adapter_groupChild() {
         Notification notification = new Notification.Builder(mContext, "")
                 .setSmallIcon(R.drawable.ic_person)
                 .setGroupSummary(true)
@@ -591,7 +591,7 @@
                 .setParent(groupEntry.build())
                 .build();
 
-        assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter());
+        assertThat(entry.getEntryAdapter().isGroupRoot()).isFalse();
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
index 543f0c7..e40d68e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
@@ -66,7 +66,7 @@
 
         assertTrue(
             "Notifs without any flags should be dismissible",
-            dismissibilityProvider.isDismissable(entry)
+            dismissibilityProvider.isDismissable(entry.key)
         )
     }
 
@@ -96,7 +96,7 @@
 
         assertFalse(
             "Non-dismiss Notifs should NOT be dismissible",
-            dismissibilityProvider.isDismissable(entry)
+            dismissibilityProvider.isDismissable(entry.key)
         )
     }
 
@@ -113,7 +113,7 @@
 
         assertFalse(
             "Ongoing Notifs should NOT be dismissible when the device is locked",
-            dismissibilityProvider.isDismissable(entry)
+            dismissibilityProvider.isDismissable(entry.key)
         )
     }
 
@@ -130,7 +130,7 @@
 
         assertTrue(
             "Ongoing Notifs should be dismissible when the device is unlocked",
-            dismissibilityProvider.isDismissable(entry)
+            dismissibilityProvider.isDismissable(entry.key)
         )
     }
 
@@ -148,7 +148,7 @@
 
         assertFalse(
             "Non-dismiss Notifs should NOT be dismissible",
-            dismissibilityProvider.isDismissable(entry)
+            dismissibilityProvider.isDismissable(entry.key)
         )
     }
 
@@ -174,16 +174,16 @@
 
         assertTrue(
             "Notifs without any flags should be dismissible",
-            dismissibilityProvider.isDismissable(noFlagEntry)
+            dismissibilityProvider.isDismissable(noFlagEntry.key)
         )
         assertTrue(
             "Ongoing Notifs should be dismissible when the device is unlocked",
-            dismissibilityProvider.isDismissable(ongoingEntry)
+            dismissibilityProvider.isDismissable(ongoingEntry.key)
         )
 
         assertFalse(
             "Non-dismiss Notifs should NOT be dismissible",
-            dismissibilityProvider.isDismissable(nonDismissEntry)
+            dismissibilityProvider.isDismissable(nonDismissEntry.key)
         )
     }
 
@@ -199,10 +199,13 @@
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(group))
 
-        assertFalse("Child should be non-dismissible", dismissibilityProvider.isDismissable(entry))
+        assertFalse(
+            "Child should be non-dismissible",
+            dismissibilityProvider.isDismissable(entry.key)
+        )
         assertFalse(
             "Summary should be non-dismissible",
-            dismissibilityProvider.isDismissable(summary)
+            dismissibilityProvider.isDismissable(summary.key)
         )
     }
 
@@ -219,10 +222,13 @@
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(group))
 
-        assertFalse("Child should be non-dismissible", dismissibilityProvider.isDismissable(entry))
+        assertFalse(
+            "Child should be non-dismissible",
+            dismissibilityProvider.isDismissable(entry.key)
+        )
         assertFalse(
             "Summary should be non-dismissible",
-            dismissibilityProvider.isDismissable(summary)
+            dismissibilityProvider.isDismissable(summary.key)
         )
     }
 
@@ -239,8 +245,11 @@
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(group))
 
-        assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry))
-        assertTrue("Summary should be dismissible", dismissibilityProvider.isDismissable(summary))
+        assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry.key))
+        assertTrue(
+            "Summary should be dismissible",
+            dismissibilityProvider.isDismissable(summary.key)
+        )
     }
 
     @Test
@@ -254,7 +263,10 @@
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(group))
 
-        assertFalse("Child should be non-dismissible", dismissibilityProvider.isDismissable(entry))
+        assertFalse(
+            "Child should be non-dismissible",
+            dismissibilityProvider.isDismissable(entry.key)
+        )
     }
 
     @Test
@@ -269,7 +281,10 @@
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(group))
 
-        assertFalse("Child should be non-dismissible", dismissibilityProvider.isDismissable(entry))
+        assertFalse(
+            "Child should be non-dismissible",
+            dismissibilityProvider.isDismissable(entry.key)
+        )
     }
 
     @Test
@@ -284,7 +299,7 @@
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(group))
 
-        assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry))
+        assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry.key))
     }
 
     @Test
@@ -299,10 +314,10 @@
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(group))
 
-        assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry))
+        assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry.key))
         assertFalse(
             "Summary should be non-dismissible",
-            dismissibilityProvider.isDismissable(summary)
+            dismissibilityProvider.isDismissable(summary.key)
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index 3dd0982..8e6aedca 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.collection.render
 
 import android.os.Build
+import android.os.UserHandle
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
@@ -29,9 +30,12 @@
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -55,56 +59,58 @@
 
     private lateinit var underTest: GroupExpansionManagerImpl
 
+    private lateinit var testHelper: NotificationTestHelper
     private val dumpManager: DumpManager = mock()
     private val groupMembershipManager: GroupMembershipManager = mock()
 
     private val pipeline: NotifPipeline = mock()
     private lateinit var beforeRenderListListener: OnBeforeRenderListListener
 
-    private val summary1 = notificationSummaryEntry("foo", 1)
-    private val summary2 = notificationSummaryEntry("bar", 1)
-    private val entries =
-        listOf<ListEntry>(
-            GroupEntryBuilder()
-                .setSummary(summary1)
-                .setChildren(
-                    listOf(
-                        notificationEntry("foo", 2),
-                        notificationEntry("foo", 3),
-                        notificationEntry("foo", 4)
-                    )
-                )
-                .build(),
-            GroupEntryBuilder()
-                .setSummary(summary2)
-                .setChildren(
-                    listOf(
-                        notificationEntry("bar", 2),
-                        notificationEntry("bar", 3),
-                        notificationEntry("bar", 4)
-                    )
-                )
-                .build(),
-            notificationEntry("baz", 1)
-        )
+    private lateinit var summary1: NotificationEntry
+    private lateinit var summary2: NotificationEntry
+    private lateinit var entries: List<ListEntry>
 
-    private fun notificationEntry(pkg: String, id: Int) =
-        NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() }
-
-    private fun notificationSummaryEntry(pkg: String, id: Int) =
-        NotificationEntryBuilder().setPkg(pkg).setId(id).setParent(GroupEntry.ROOT_ENTRY).build()
-            .apply { row = mock() }
+    private fun notificationEntry(pkg: String, id: Int, parent: ExpandableNotificationRow?) =
+        NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply {
+            row = testHelper.createRow().apply {
+                setIsChildInGroup(true, parent)
+            }
+        }
 
     @Before
     fun setUp() {
+        testHelper = NotificationTestHelper(mContext, mDependency)
+
+        summary1 = testHelper.createRow().entry
+        summary2 = testHelper.createRow().entry
+        entries =
+            listOf<ListEntry>(
+                GroupEntryBuilder()
+                    .setSummary(summary1)
+                    .setChildren(
+                        listOf(
+                            notificationEntry("foo", 2, summary1.row),
+                            notificationEntry("foo", 3, summary1.row),
+                            notificationEntry("foo", 4, summary1.row)
+                        )
+                    )
+                    .build(),
+                GroupEntryBuilder()
+                    .setSummary(summary2)
+                    .setChildren(
+                        listOf(
+                            notificationEntry("bar", 2, summary2.row),
+                            notificationEntry("bar", 3, summary2.row),
+                            notificationEntry("bar", 4, summary2.row)
+                        )
+                    )
+                    .build(),
+                notificationEntry("baz", 1, null)
+            )
+
         whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1)
         whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2)
 
-        whenever(groupMembershipManager.getGroupRoot(summary1.entryAdapter))
-            .thenReturn(summary1.entryAdapter)
-        whenever(groupMembershipManager.getGroupRoot(summary2.entryAdapter))
-            .thenReturn(summary2.entryAdapter)
-
         underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager)
     }
 
@@ -221,4 +227,15 @@
         verify(listener).onGroupExpansionChange(summary1.row, false)
         verifyNoMoreInteractions(listener)
     }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isGroupExpanded() {
+        underTest.setGroupExpanded(summary1.entryAdapter, true)
+
+        assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue();
+        assertThat(underTest.isGroupExpanded(
+            (entries[0] as? GroupEntry)?.getChildren()?.get(0)?.entryAdapter))
+            .isTrue();
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
index dcbf44e..2bbf094 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
@@ -170,6 +170,21 @@
 
     @Test
     @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun isChildEntryAdapterInGroup_child() {
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+        assertThat(underTest.isChildInGroup(entry.entryAdapter)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
     fun isGroupRoot_topLevelEntry() {
         val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
         assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse()
@@ -203,40 +218,4 @@
 
         assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse()
     }
-
-    @Test
-    @EnableFlags(NotificationBundleUi.FLAG_NAME)
-    fun getGroupRoot_topLevelEntry() {
-        val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
-        assertThat(underTest.getGroupRoot(entry.entryAdapter)).isNull()
-    }
-
-    @Test
-    @EnableFlags(NotificationBundleUi.FLAG_NAME)
-    fun getGroupRoot_summary() {
-        val groupKey = "group"
-        val summary =
-            NotificationEntryBuilder()
-                .setGroup(mContext, groupKey)
-                .setGroupSummary(mContext, true)
-                .build()
-        GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
-
-        assertThat(underTest.getGroupRoot(summary.entryAdapter)).isEqualTo(summary.entryAdapter)
-    }
-
-    @Test
-    @EnableFlags(NotificationBundleUi.FLAG_NAME)
-    fun getGroupRoot_child() {
-        val groupKey = "group"
-        val summary =
-            NotificationEntryBuilder()
-                .setGroup(mContext, groupKey)
-                .setGroupSummary(mContext, true)
-                .build()
-        val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
-        GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
-
-        assertThat(underTest.getGroupRoot(entry.entryAdapter)).isEqualTo(summary.entryAdapter)
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 7603eec..33211d4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.RoundableState
+import com.android.systemui.statusbar.notification.collection.EntryAdapter
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
@@ -56,6 +57,7 @@
     private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
     private val notificationRow = mock<ExpandableNotificationRow>()
     private val notificationEntry = mock<NotificationEntry>()
+    private val notificationEntryAdapter = mock<EntryAdapter>()
     private val dumpManager = mock<DumpManager>()
     private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
     private val notificationShelf = mock<NotificationShelf>()
@@ -109,8 +111,10 @@
         Assume.assumeFalse(isTv())
         mDependency.injectTestDependency(FeatureFlags::class.java, featureFlags)
         whenever(notificationShelf.viewState).thenReturn(ExpandableViewState())
+        whenever(notificationRow.key).thenReturn("key")
         whenever(notificationRow.viewState).thenReturn(ExpandableViewState())
         whenever(notificationRow.entry).thenReturn(notificationEntry)
+        whenever(notificationRow.entryAdapter).thenReturn(notificationEntryAdapter)
         whenever(notificationRow.roundableState)
             .thenReturn(RoundableState(notificationRow, notificationRow, 0f))
         ambientState.isSmallScreen = true
@@ -452,7 +456,11 @@
     @Test
     fun resetViewStates_hunsOverlapping_bottomHunClipped() {
         val topHun = mockExpandableNotificationRow()
+        whenever(topHun.key).thenReturn("key")
+        whenever(topHun.entryAdapter).thenReturn(notificationEntryAdapter)
         val bottomHun = mockExpandableNotificationRow()
+        whenever(bottomHun.key).thenReturn("key")
+        whenever(bottomHun.entryAdapter).thenReturn(notificationEntryAdapter)
         whenever(topHun.isHeadsUp).thenReturn(true)
         whenever(topHun.isPinned).thenReturn(true)
         whenever(bottomHun.isHeadsUp).thenReturn(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt
index bd76268..6a56716 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt
@@ -388,7 +388,7 @@
 
         // Pulsing: Enabled
         row.isHeadsUp = true
-        underTest.updateHeadsUpAndPulsingRoundness(entry)
+        underTest.updateHeadsUpAndPulsingRoundness(row)
 
         val debugString: String = row.roundableState.debugString()
         // If Pulsing is enabled, roundness should be set to 1
@@ -397,7 +397,7 @@
 
         // Pulsing: Disabled
         row.isHeadsUp = false
-        underTest.updateHeadsUpAndPulsingRoundness(entry)
+        underTest.updateHeadsUpAndPulsingRoundness(row)
 
         // If Pulsing is disabled, roundness should be set to 0
         assertThat(row.topRoundness.toDouble()).isWithin(0.001).of(0.0)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt
index baea1a1..47967b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt
@@ -54,6 +54,7 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
 import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.visualInterruptionDecisionProvider
 import com.android.systemui.statusbar.notificationLockscreenUserManager
@@ -293,6 +294,7 @@
 
     @Test
     @EnableSceneContainer
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun testExpandSensitiveNotification_onLockScreen_opensShade() =
         kosmos.runTest {
             // Given we are on the keyguard
@@ -303,11 +305,10 @@
             )
 
             // When the user expands a sensitive Notification
-            val row = createRow()
-            val entry =
-                row.entry.apply { setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true) }
+            val row = createRow(createNotificationEntry())
+            row.entry.apply { setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true) }
 
-            underTest.onExpandClicked(entry, mock(), /* nowExpanded= */ true)
+            underTest.onExpandClicked(row.entry, mock(), /* nowExpanded= */ true)
 
             // Then we open the locked shade
             verify(kosmos.lockscreenShadeTransitionController)
@@ -317,6 +318,32 @@
 
     @Test
     @EnableSceneContainer
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun testExpandSensitiveNotification_onLockScreen_opensShade_entryAdapter() =
+        kosmos.runTest {
+            // Given we are on the keyguard
+            kosmos.sysuiStatusBarStateController.state = StatusBarState.KEYGUARD
+            // And the device is locked
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+
+            // When the user expands a sensitive Notification
+            val entry = createNotificationEntry()
+            val row = createRow(entry)
+            entry.setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true)
+
+            underTest.onExpandClicked(row, row.entryAdapter, /* nowExpanded= */ true)
+
+            // Then we open the locked shade
+            verify(kosmos.lockscreenShadeTransitionController)
+                // Explicit parameters to avoid issues with Kotlin default arguments in Mockito
+                .goToLockedShade(row, true)
+        }
+
+    @Test
+    @EnableSceneContainer
+    @DisableFlags(NotificationBundleUi.FLAG_NAME)
     fun testExpandSensitiveNotification_onLockedShade_showsBouncer() =
         kosmos.runTest {
             // Given we are on the locked shade
@@ -328,7 +355,7 @@
 
             // When the user expands a sensitive Notification
             val entry =
-                createRow().entry.apply {
+                createRow(createNotificationEntry()).entry.apply {
                     setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true)
                 }
             underTest.onExpandClicked(entry, mock(), /* nowExpanded= */ true)
@@ -337,6 +364,29 @@
             verify(kosmos.activityStarter).dismissKeyguardThenExecute(any(), eq(null), eq(false))
         }
 
+    @Test
+    @EnableSceneContainer
+    @EnableFlags(NotificationBundleUi.FLAG_NAME)
+    fun testExpandSensitiveNotification_onLockedShade_showsBouncer_entryAdapter() =
+        kosmos.runTest {
+            // Given we are on the locked shade
+            kosmos.sysuiStatusBarStateController.state = StatusBarState.SHADE_LOCKED
+            // And the device is locked
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+
+            // When the user expands a sensitive Notification
+            val entry = createNotificationEntry()
+            val row = createRow(entry)
+            entry.setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true)
+
+            underTest.onExpandClicked(row, row.entryAdapter, /* nowExpanded= */ true)
+
+            // Then we show the bouncer
+            verify(kosmos.activityStarter).dismissKeyguardThenExecute(any(), eq(null), eq(false))
+        }
+
     private fun createPresenter(): StatusBarNotificationPresenter {
         val initController: InitController = InitController()
         return StatusBarNotificationPresenter(
@@ -398,10 +448,13 @@
         interruptSuppressor = suppressorCaptor.lastValue
     }
 
-    private fun createRow(): ExpandableNotificationRow {
+    private fun createRow(entry: NotificationEntry): ExpandableNotificationRow {
         val row: ExpandableNotificationRow = mock()
-        val entry: NotificationEntry = createNotificationEntry()
-        whenever(row.entry).thenReturn(entry)
+        if (NotificationBundleUi.isEnabled) {
+            whenever(row.entryAdapter).thenReturn(entry.entryAdapter)
+        } else {
+            whenever(row.entry).thenReturn(entry)
+        }
         entry.row = row
         return row
     }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
index 6e4dc14..0cbc30d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
@@ -34,6 +34,9 @@
 
     /** Font axes that can be modified on this clock */
     val axes: List<ClockFontAxis> = listOf(),
+
+    /** List of font presets for this clock. Can be assigned directly. */
+    val axisPresets: List<List<ClockFontAxisSetting>> = listOf(),
 )
 
 /** Represents an Axis that can be modified */
diff --git a/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml
new file mode 100644
index 0000000..1de8c2b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle" >
+    <solid
+        android:color="@color/notification_guts_priority_button_bg_fill" />
+
+    <stroke
+        android:width="1.5dp"
+        android:color="@color/notification_guts_priority_button_bg_stroke" />
+
+    <corners android:radius="16dp" />
+</shape>
diff --git a/packages/SystemUI/res/layout/notification_2025_info.xml b/packages/SystemUI/res/layout/notification_2025_info.xml
new file mode 100644
index 0000000..7b69166
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_info.xml
@@ -0,0 +1,365 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 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.
+-->
+
+<!-- extends LinearLayout -->
+<com.android.systemui.statusbar.notification.row.NotificationInfo
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/notification_guts"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:focusable="true"
+    android:clipChildren="false"
+    android:clipToPadding="true"
+    android:orientation="vertical"
+    android:paddingStart="@*android:dimen/notification_2025_margin">
+
+    <!-- Package Info -->
+    <LinearLayout
+        android:id="@+id/header"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:clipChildren="false"
+        android:clipToPadding="true">
+        <ImageView
+            android:id="@+id/pkg_icon"
+            android:layout_width="@*android:dimen/notification_2025_icon_circle_size"
+            android:layout_height="@*android:dimen/notification_2025_icon_circle_size"
+            android:layout_marginTop="@*android:dimen/notification_2025_margin"
+            android:layout_marginEnd="@*android:dimen/notification_2025_margin" />
+        <LinearLayout
+            android:id="@+id/names"
+            android:layout_weight="1"
+            android:layout_width="0dp"
+            android:orientation="vertical"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@*android:dimen/notification_2025_margin"
+            android:minHeight="@*android:dimen/notification_2025_icon_circle_size">
+            <TextView
+                android:id="@+id/channel_name"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textDirection="locale"
+                style="@style/TextAppearance.NotificationImportanceChannel"/>
+            <TextView
+                android:id="@+id/group_name"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textDirection="locale"
+                android:ellipsize="end"
+                style="@style/TextAppearance.NotificationImportanceChannelGroup"/>
+            <TextView
+                android:id="@+id/pkg_name"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                style="@style/TextAppearance.NotificationImportanceApp"
+                android:ellipsize="end"
+                android:textDirection="locale"
+                android:maxLines="1"/>
+            <TextView
+                android:id="@+id/delegate_name"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                style="@style/TextAppearance.NotificationImportanceHeader"
+                android:ellipsize="end"
+                android:textDirection="locale"
+                android:text="@string/notification_delegate_header"
+                android:maxLines="1" />
+
+        </LinearLayout>
+
+        <!-- feedback for notificationassistantservice -->
+        <ImageButton
+            android:id="@+id/feedback"
+            android:layout_width="@dimen/notification_2025_guts_button_size"
+            android:layout_height="@dimen/notification_2025_guts_button_size"
+            android:visibility="gone"
+            android:background="@drawable/ripple_drawable"
+            android:contentDescription="@string/notification_guts_bundle_feedback"
+            android:src="@*android:drawable/ic_feedback"
+            android:paddingTop="@*android:dimen/notification_2025_margin"
+            android:tint="@androidprv:color/materialColorPrimary"/>
+
+        <!-- Optional link to app. Only appears if the channel is not disabled and the app
+        asked for it -->
+        <ImageButton
+            android:id="@+id/app_settings"
+            android:layout_width="@dimen/notification_2025_guts_button_size"
+            android:layout_height="@dimen/notification_2025_guts_button_size"
+            android:visibility="gone"
+            android:background="@drawable/ripple_drawable"
+            android:contentDescription="@string/notification_app_settings"
+            android:src="@drawable/ic_info"
+            android:paddingTop="@*android:dimen/notification_2025_margin"
+            android:tint="@androidprv:color/materialColorPrimary"/>
+
+        <!-- System notification settings -->
+        <ImageButton
+            android:id="@+id/info"
+            android:layout_width="@dimen/notification_2025_guts_button_size"
+            android:layout_height="@dimen/notification_2025_guts_button_size"
+            android:contentDescription="@string/notification_more_settings"
+            android:background="@drawable/ripple_drawable"
+            android:src="@drawable/ic_settings"
+            android:padding="@*android:dimen/notification_2025_margin"
+            android:tint="@androidprv:color/materialColorPrimary" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/inline_controls"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@*android:dimen/notification_2025_margin"
+        android:layout_marginTop="@*android:dimen/notification_2025_margin"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        android:orientation="vertical">
+
+        <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+        <TextView
+            android:id="@+id/non_configurable_text"
+            android:text="@string/notification_unblockable_desc"
+            android:visibility="gone"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+        <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+        <TextView
+            android:id="@+id/non_configurable_call_text"
+            android:text="@string/notification_unblockable_call_desc"
+            android:visibility="gone"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+        <!-- Non configurable multichannel text. appears instead of @+id/interruptiveness_settings-->
+        <TextView
+            android:id="@+id/non_configurable_multichannel_text"
+            android:text="@string/notification_multichannel_desc"
+            android:visibility="gone"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+        <LinearLayout
+            android:id="@+id/interruptiveness_settings"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:orientation="vertical">
+            <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+                android:id="@+id/automatic"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+                android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+                android:gravity="center_vertical"
+                android:clickable="true"
+                android:focusable="true"
+                android:background="@drawable/notification_2025_guts_priority_button_bg"
+                android:orientation="horizontal"
+                android:visibility="gone">
+                <ImageView
+                    android:id="@+id/automatic_icon"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:paddingEnd="@*android:dimen/notification_2025_margin"
+                    android:src="@drawable/ic_notifications_automatic"
+                    android:background="@android:color/transparent"
+                    android:tint="@color/notification_guts_priority_contents"
+                    android:clickable="false"
+                    android:focusable="false"/>
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical"
+                    android:gravity="center"
+                >
+                    <TextView
+                        android:id="@+id/automatic_label"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1"
+                        android:ellipsize="end"
+                        android:maxLines="1"
+                        android:clickable="false"
+                        android:focusable="false"
+                        android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+                        android:text="@string/notification_automatic_title"/>
+                    <TextView
+                        android:id="@+id/automatic_summary"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+                        android:visibility="gone"
+                        android:text="@string/notification_channel_summary_automatic"
+                        android:clickable="false"
+                        android:focusable="false"
+                        android:ellipsize="end"
+                        android:maxLines="2"
+                        android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+                </LinearLayout>
+            </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+            <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+                android:id="@+id/alert"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+                android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+                android:gravity="center_vertical"
+                android:clickable="true"
+                android:focusable="true"
+                android:background="@drawable/notification_2025_guts_priority_button_bg"
+                android:orientation="horizontal">
+                <ImageView
+                    android:id="@+id/alert_icon"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:paddingEnd="@*android:dimen/notification_2025_margin"
+                    android:src="@drawable/ic_notifications_alert"
+                    android:background="@android:color/transparent"
+                    android:tint="@color/notification_guts_priority_contents"
+                    android:clickable="false"
+                    android:focusable="false"/>
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical"
+                    android:gravity="center"
+                    >
+                    <TextView
+                        android:id="@+id/alert_label"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1"
+                        android:ellipsize="end"
+                        android:maxLines="1"
+                        android:clickable="false"
+                        android:focusable="false"
+                        android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+                        android:text="@string/notification_alert_title"/>
+                    <TextView
+                        android:id="@+id/alert_summary"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:visibility="gone"
+                        android:text="@string/notification_channel_summary_default"
+                        android:clickable="false"
+                        android:focusable="false"
+                        android:ellipsize="end"
+                        android:maxLines="2"
+                        android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+                </LinearLayout>
+            </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+            <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+                android:id="@+id/silence"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/notification_importance_button_separation"
+                android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+                android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+                android:gravity="center_vertical"
+                android:clickable="true"
+                android:focusable="true"
+                android:background="@drawable/notification_2025_guts_priority_button_bg"
+                android:orientation="horizontal">
+                <ImageView
+                    android:id="@+id/silence_icon"
+                    android:src="@drawable/ic_notifications_silence"
+                    android:background="@android:color/transparent"
+                    android:tint="@color/notification_guts_priority_contents"
+                    android:layout_gravity="center"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:paddingEnd="@*android:dimen/notification_2025_margin"
+                    android:clickable="false"
+                    android:focusable="false"/>
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical"
+                    android:gravity="center"
+                    >
+                    <TextView
+                        android:id="@+id/silence_label"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:ellipsize="end"
+                        android:maxLines="1"
+                        android:clickable="false"
+                        android:focusable="false"
+                        android:layout_toEndOf="@id/silence_icon"
+                        android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+                        android:text="@string/notification_silence_title"/>
+                    <TextView
+                        android:id="@+id/silence_summary"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:visibility="gone"
+                        android:text="@string/notification_channel_summary_low"
+                        android:clickable="false"
+                        android:focusable="false"
+                        android:ellipsize="end"
+                        android:maxLines="2"
+                        android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+                </LinearLayout>
+            </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/bottom_buttons"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@*android:dimen/notification_2025_margin"
+            android:minHeight="@dimen/notification_2025_guts_button_size"
+            android:gravity="center_vertical"
+            >
+            <TextView
+                android:id="@+id/turn_off_notifications"
+                android:text="@string/inline_turn_off_notifications"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="32dp"
+                android:paddingTop="8dp"
+                android:paddingBottom="@*android:dimen/notification_2025_margin"
+                android:gravity="center"
+                android:minWidth="@dimen/notification_2025_min_tap_target_size"
+                android:minHeight="@dimen/notification_2025_min_tap_target_size"
+                android:maxWidth="200dp"
+                style="@style/TextAppearance.NotificationInfo.Button"
+                android:textSize="@*android:dimen/notification_2025_action_text_size"/>
+            <TextView
+                android:id="@+id/done"
+                android:text="@string/inline_ok_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingTop="8dp"
+                android:paddingBottom="@*android:dimen/notification_2025_margin"
+                android:gravity="center"
+                android:minWidth="@dimen/notification_2025_min_tap_target_size"
+                android:minHeight="@dimen/notification_2025_min_tap_target_size"
+                android:maxWidth="125dp"
+                style="@style/TextAppearance.NotificationInfo.Button"
+                android:textSize="@*android:dimen/notification_2025_action_text_size"/>
+        </LinearLayout>
+    </LinearLayout>
+</com.android.systemui.statusbar.notification.row.NotificationInfo>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index b273886..4995858 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1125,4 +1125,7 @@
 
     <!-- Configuration to swipe to open glanceable hub -->
     <bool name="config_swipeToOpenGlanceableHub">false</bool>
+
+    <!-- Whether or not to show the UMO on the glanceable hub when media is playing. -->
+    <bool name="config_showUmoOnHub">false</bool>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 640e1fa..7c370d3 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -390,6 +390,12 @@
     <!-- Extra space for guts bundle feedback button -->
     <dimen name="notification_guts_bundle_feedback_size">48dp</dimen>
 
+    <!-- Size of icon buttons in notification info. -->
+    <!-- 24dp for the icon itself + 16dp * 2 for top and bottom padding -->
+    <dimen name="notification_2025_guts_button_size">56dp</dimen>
+
+    <dimen name="notification_2025_min_tap_target_size">48dp</dimen>
+
     <dimen name="notification_importance_toggle_size">48dp</dimen>
     <dimen name="notification_importance_button_separation">8dp</dimen>
     <dimen name="notification_importance_drawable_padding">8dp</dimen>
@@ -402,6 +408,10 @@
     <dimen name="notification_importance_button_description_top_margin">12dp</dimen>
     <dimen name="rect_button_radius">8dp</dimen>
 
+    <!-- Padding for importance selection buttons in notification info, 2025 redesign version -->
+    <dimen name="notification_2025_importance_button_padding_vertical">12dp</dimen>
+    <dimen name="notification_2025_importance_button_padding_horizontal">16dp</dimen>
+
     <!-- The minimum height for the snackbar shown after the snooze option has been chosen. -->
     <dimen name="snooze_snackbar_min_height">56dp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 084495f..6ff1240 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3176,8 +3176,8 @@
     <string name="controls_media_settings_button">Settings</string>
     <!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]-->
     <string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
-    <!-- Content description for media controls progress bar [CHAR_LIMIT=NONE] -->
-    <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1 hour 2 minutes 30 seconds">%1$s</xliff:g> of <xliff:g id="total_time" example="4 hours 5 seconds">%2$s</xliff:g></string>
+    <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
+    <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
     <!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] -->
     <string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string>
 
@@ -4178,4 +4178,7 @@
     <string name="qs_edit_mode_reset_dialog_content">
         All Quick Settings tiles will reset to the device’s original settings
     </string>
+
+    <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] -->
+    <string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
index 208adc2..5f7dca8 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
@@ -64,15 +64,14 @@
                         synchronized(sessions) {
                             sessions.putAll(
                                 packageInstaller.allSessions
-                                    .filter { !TextUtils.isEmpty(it.appPackageName) }
-                                    .map { session -> session.toModel() }
+                                    .mapNotNull { session -> session.toModel() }
                                     .associateBy { it.sessionId }
                             )
                             updateInstallerSessionsFlow()
                         }
                         packageInstaller.registerSessionCallback(
                             this@PackageInstallerMonitor,
-                            bgHandler
+                            bgHandler,
                         )
                     } else {
                         synchronized(sessions) {
@@ -130,7 +129,7 @@
             if (session == null) {
                 sessions.remove(sessionId)
             } else {
-                sessions[sessionId] = session.toModel()
+                session.toModel()?.apply { sessions[sessionId] = this }
             }
             updateInstallerSessionsFlow()
         }
@@ -144,7 +143,11 @@
     companion object {
         const val TAG = "PackageInstallerMonitor"
 
-        private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession {
+        private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession? {
+            if (TextUtils.isEmpty(this.appPackageName)) {
+                return null
+            }
+
             return PackageInstallSession(
                 sessionId = this.sessionId,
                 packageName = this.appPackageName,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
index 48a6d9d..7765d00 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.communal
 
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.CoreStartable
+import com.android.systemui.communal.dagger.CommunalModule.Companion.SHOW_UMO
 import com.android.systemui.communal.data.repository.CommunalMediaRepository
 import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -24,8 +26,8 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import javax.inject.Inject
+import javax.inject.Named
 import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 @SysUISingleton
 class CommunalOngoingContentStartable
@@ -36,6 +38,7 @@
     private val communalMediaRepository: CommunalMediaRepository,
     private val communalSettingsInteractor: CommunalSettingsInteractor,
     private val communalSmartspaceRepository: CommunalSmartspaceRepository,
+    @Named(SHOW_UMO) private val showUmoOnHub: Boolean,
 ) : CoreStartable {
 
     override fun start() {
@@ -46,10 +49,14 @@
         bgScope.launch {
             communalInteractor.isCommunalEnabled.collect { enabled ->
                 if (enabled) {
-                    communalMediaRepository.startListening()
+                    if (showUmoOnHub) {
+                        communalMediaRepository.startListening()
+                    }
                     communalSmartspaceRepository.startListening()
                 } else {
-                    communalMediaRepository.stopListening()
+                    if (showUmoOnHub) {
+                        communalMediaRepository.stopListening()
+                    }
                     communalSmartspaceRepository.stopListening()
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index ff74162..bb3be53 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -105,6 +105,7 @@
         const val LOGGABLE_PREFIXES = "loggable_prefixes"
         const val LAUNCHER_PACKAGE = "launcher_package"
         const val SWIPE_TO_HUB = "swipe_to_hub"
+        const val SHOW_UMO = "show_umo"
 
         @Provides
         @Communal
@@ -150,5 +151,11 @@
         fun provideSwipeToHub(@Main resources: Resources): Boolean {
             return resources.getBoolean(R.bool.config_swipeToOpenGlanceableHub)
         }
+
+        @Provides
+        @Named(SHOW_UMO)
+        fun provideShowUmo(@Main resources: Resources): Boolean {
+            return resources.getBoolean(R.bool.config_showUmoOnHub)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 2650159..15a4722 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -29,8 +29,6 @@
 import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
 import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
@@ -52,8 +50,6 @@
         NotificationMinimalism.token dependsOn NotificationThrottleHun.token
         ModesEmptyShadeFix.token dependsOn modesUi
 
-        PromotedNotificationUiAod.token dependsOn PromotedNotificationUi.token
-
         // SceneContainer dependencies
         SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index d3b76a5..a42682b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockPreviewConfig
 import com.android.systemui.shared.clocks.ClockRegistry
+import kotlinx.coroutines.flow.combine
 
 /** Binder for the small clock view, large clock view. */
 object KeyguardPreviewClockViewBinder {
@@ -76,38 +77,39 @@
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 var lastClock: ClockController? = null
                 launch("$TAG#viewModel.previewClock") {
-                        viewModel.previewClock.collect { currentClock ->
-                            lastClock?.let { clock ->
-                                (clock.largeClock.layout.views + clock.smallClock.layout.views)
-                                    .forEach { rootView.removeView(it) }
-                            }
-                            lastClock = currentClock
-                            updateClockAppearance(
-                                currentClock,
-                                clockPreviewConfig.context.resources,
-                            )
+                        combine(viewModel.previewClock, viewModel.selectedClockSize, ::Pair)
+                            .collect { (currentClock, clockSize) ->
+                                lastClock?.let { clock ->
+                                    (clock.largeClock.layout.views + clock.smallClock.layout.views)
+                                        .forEach { rootView.removeView(it) }
+                                }
+                                lastClock = currentClock
+                                updateClockAppearance(
+                                    currentClock,
+                                    clockPreviewConfig.context.resources,
+                                )
 
-                            if (viewModel.shouldHighlightSelectedAffordance) {
-                                (currentClock.largeClock.layout.views +
-                                        currentClock.smallClock.layout.views)
-                                    .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
-                            }
-                            currentClock.largeClock.layout.views.forEach {
-                                (it.parent as? ViewGroup)?.removeView(it)
-                                rootView.addView(it)
-                            }
+                                if (viewModel.shouldHighlightSelectedAffordance) {
+                                    (currentClock.largeClock.layout.views +
+                                            currentClock.smallClock.layout.views)
+                                        .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
+                                }
+                                currentClock.largeClock.layout.views.forEach {
+                                    (it.parent as? ViewGroup)?.removeView(it)
+                                    rootView.addView(it)
+                                }
 
-                            currentClock.smallClock.layout.views.forEach {
-                                (it.parent as? ViewGroup)?.removeView(it)
-                                rootView.addView(it)
+                                currentClock.smallClock.layout.views.forEach {
+                                    (it.parent as? ViewGroup)?.removeView(it)
+                                    rootView.addView(it)
+                                }
+                                applyPreviewConstraints(
+                                    clockPreviewConfig,
+                                    rootView,
+                                    currentClock,
+                                    clockSize,
+                                )
                             }
-                            applyPreviewConstraints(
-                                clockPreviewConfig,
-                                rootView,
-                                currentClock,
-                                viewModel,
-                            )
-                        }
                     }
                     .invokeOnCompletion {
                         // recover seed color especially for Transit clock
@@ -133,7 +135,7 @@
         clockPreviewConfig: ClockPreviewConfig,
         rootView: ConstraintLayout,
         previewClock: ClockController,
-        viewModel: KeyguardPreviewClockViewModel,
+        clockSize: ClockSizeSetting?,
     ) {
         val cs = ConstraintSet().apply { clone(rootView) }
 
@@ -147,16 +149,15 @@
         previewClock.largeClock.layout.applyPreviewConstraints(configWithUpdatedLockId, cs)
         previewClock.smallClock.layout.applyPreviewConstraints(configWithUpdatedLockId, cs)
 
-        // When selectedClockSize is the initial value, make both clocks invisible to avoid
-        // flickering
+        // When selectedClockSize is the initial value, make both clocks invisible to avoid flicker
         val largeClockVisibility =
-            when (viewModel.selectedClockSize.value) {
+            when (clockSize) {
                 ClockSizeSetting.DYNAMIC -> VISIBLE
                 ClockSizeSetting.SMALL -> INVISIBLE
                 null -> INVISIBLE
             }
         val smallClockVisibility =
-            when (viewModel.selectedClockSize.value) {
+            when (clockSize) {
                 ClockSizeSetting.DYNAMIC -> INVISIBLE
                 ClockSizeSetting.SMALL -> VISIBLE
                 null -> INVISIBLE
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
index c9716be..34f7c4d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
@@ -18,9 +18,6 @@
 
 import android.animation.Animator
 import android.animation.ObjectAnimator
-import android.icu.text.MeasureFormat
-import android.icu.util.Measure
-import android.icu.util.MeasureUnit
 import android.text.format.DateUtils
 import androidx.annotation.UiThread
 import androidx.lifecycle.Observer
@@ -31,11 +28,8 @@
 import com.android.systemui.media.controls.ui.view.MediaViewHolder
 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
 import com.android.systemui.res.R
-import java.util.Locale
 
 private const val TAG = "SeekBarObserver"
-private const val MIN_IN_SEC = 60
-private const val HOUR_IN_SEC = MIN_IN_SEC * 60
 
 /**
  * Observer for changes from SeekBarViewModel.
@@ -133,9 +127,10 @@
         }
 
         holder.seekBar.setMax(data.duration)
-        val totalTimeDescription = formatTimeContentDescription(data.duration)
+        val totalTimeString =
+            DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
         if (data.scrubbing) {
-            holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration)
+            holder.scrubbingTotalTimeView.text = totalTimeString
         }
 
         data.elapsedTime?.let {
@@ -153,62 +148,20 @@
                 }
             }
 
-            val elapsedTimeDescription = formatTimeContentDescription(it)
+            val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
             if (data.scrubbing) {
-                holder.scrubbingElapsedTimeView.text = formatTimeLabel(it)
+                holder.scrubbingElapsedTimeView.text = elapsedTimeString
             }
 
             holder.seekBar.contentDescription =
                 holder.seekBar.context.getString(
                     R.string.controls_media_seekbar_description,
-                    elapsedTimeDescription,
-                    totalTimeDescription,
+                    elapsedTimeString,
+                    totalTimeString
                 )
         }
     }
 
-    /** Returns a time string suitable for display, e.g. "12:34" */
-    private fun formatTimeLabel(milliseconds: Int): CharSequence {
-        return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS)
-    }
-
-    /**
-     * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds"
-     *
-     * Follows same logic as Chronometer#formatDuration
-     */
-    private fun formatTimeContentDescription(milliseconds: Int): CharSequence {
-        var seconds = milliseconds / DateUtils.SECOND_IN_MILLIS
-
-        val hours =
-            if (seconds >= HOUR_IN_SEC) {
-                seconds / HOUR_IN_SEC
-            } else {
-                0
-            }
-        seconds -= hours * HOUR_IN_SEC
-
-        val minutes =
-            if (seconds >= MIN_IN_SEC) {
-                seconds / MIN_IN_SEC
-            } else {
-                0
-            }
-        seconds -= minutes * MIN_IN_SEC
-
-        val measures = arrayListOf<Measure>()
-        if (hours > 0) {
-            measures.add(Measure(hours, MeasureUnit.HOUR))
-        }
-        if (minutes > 0) {
-            measures.add(Measure(minutes, MeasureUnit.MINUTE))
-        }
-        measures.add(Measure(seconds, MeasureUnit.SECOND))
-
-        return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
-            .formatMeasures(*measures.toTypedArray())
-    }
-
     @VisibleForTesting
     open fun buildResetAnimator(targetTime: Int): Animator {
         val animator =
@@ -216,7 +169,7 @@
                 holder.seekBar,
                 "progress",
                 holder.seekBar.progress,
-                targetTime + RESET_ANIMATION_DURATION_MS,
+                targetTime + RESET_ANIMATION_DURATION_MS
             )
         animator.setAutoCancel(true)
         animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 9d37580..1f2f571 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -648,6 +648,10 @@
             final MediaDevice connectedMediaDevice =
                     needToHandleMutingExpectedDevice ? null
                             : getCurrentConnectedMediaDevice();
+
+            Set<String> selectedDevicesIds = getSelectedMediaDevice().stream()
+                    .map(MediaDevice::getId)
+                    .collect(Collectors.toSet());
             if (oldMediaItems.isEmpty()) {
                 if (connectedMediaDevice == null) {
                     if (DEBUG) {
@@ -656,12 +660,14 @@
                     return categorizeMediaItemsLocked(
                             /* connectedMediaDevice */ null,
                             devices,
+                            selectedDevicesIds,
                             needToHandleMutingExpectedDevice);
                 } else {
                     // selected device exist
                     return categorizeMediaItemsLocked(
                             connectedMediaDevice,
                             devices,
+                            selectedDevicesIds,
                             /* needToHandleMutingExpectedDevice */ false);
                 }
             }
@@ -695,9 +701,20 @@
                 devices.removeAll(targetMediaDevices);
                 targetMediaDevices.addAll(devices);
             }
-            List<MediaItem> finalMediaItems = targetMediaDevices.stream()
-                    .map(MediaItem::createDeviceMediaItem)
-                    .collect(Collectors.toList());
+            List<MediaItem> finalMediaItems = new ArrayList<>();
+            boolean shouldAddFirstSeenSelectedDevice =
+                    com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping();
+            for (MediaDevice targetMediaDevice : targetMediaDevices) {
+                if (shouldAddFirstSeenSelectedDevice
+                        && selectedDevicesIds.contains(targetMediaDevice.getId())) {
+                    finalMediaItems.add(MediaItem.createDeviceMediaItem(
+                            targetMediaDevice, /* isFirstDeviceInGroup */ true));
+                    shouldAddFirstSeenSelectedDevice = false;
+                } else {
+                    finalMediaItems.add(MediaItem.createDeviceMediaItem(
+                            targetMediaDevice, /* isFirstDeviceInGroup */ false));
+                }
+            }
             dividerItems.forEach(finalMediaItems::add);
             attachConnectNewDeviceItemIfNeeded(finalMediaItems);
             return finalMediaItems;
@@ -724,11 +741,9 @@
     @GuardedBy("mMediaDevicesLock")
     private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice,
             List<MediaDevice> devices,
+            Set<String> selectedDevicesIds,
             boolean needToHandleMutingExpectedDevice) {
         List<MediaItem> finalMediaItems = new ArrayList<>();
-        Set<String> selectedDevicesIds = getSelectedMediaDevice().stream()
-                .map(MediaDevice::getId)
-                .collect(Collectors.toSet());
         if (connectedMediaDevice != null) {
             selectedDevicesIds.add(connectedMediaDevice.getId());
         }
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 71eacdf..e0b93fb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -252,8 +252,12 @@
                     Box(
                         modifier =
                             Modifier.graphicsLayer { alpha = viewModel.viewAlpha }
+                                .thenIf(notificationScrimClippingParams.isEnabled) {
+                                    Modifier.notificationScrimClip {
+                                        notificationScrimClippingParams.params
+                                    }
+                                }
                                 .thenIf(!Flags.notificationShadeBlur()) {
-                                    // Clipping before translation to match QSContainerImpl.onDraw
                                     Modifier.offset {
                                         IntOffset(
                                             x = 0,
@@ -261,11 +265,6 @@
                                         )
                                     }
                                 }
-                                .thenIf(notificationScrimClippingParams.isEnabled) {
-                                    Modifier.notificationScrimClip {
-                                        notificationScrimClippingParams.params
-                                    }
-                                }
                                 // Disable touches in the whole composable while the mirror is
                                 // showing. While the mirror is showing, an ancestor of the
                                 // ComposeView is made alpha 0, but touches are still being captured
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
index f80b8fb..e48e943 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
@@ -99,7 +99,7 @@
     }
 
     override fun getDetailsViewModel(): TileDetailsViewModel {
-        return internetDetailsViewModelFactory.create { longClick(null) }
+        return internetDetailsViewModelFactory.create()
     }
 
     override fun handleUpdateState(state: QSTile.BooleanState, arg: Any?) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
index e8c4274..8ad4e16 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
@@ -28,17 +28,4 @@
      * It's safe to run long running computations inside this function.
      */
     @WorkerThread suspend fun handleInput(input: QSTileInput<DATA_TYPE>)
-
-    /**
-     * Provides the [TileDetailsViewModel] for constructing the corresponding details view.
-     *
-     * This property is defined here to reuse the business logic. For example, reusing the user
-     * long-click as the go-to-settings callback in the details view.
-     * Subclasses can override this property to provide a specific [TileDetailsViewModel]
-     * implementation.
-     *
-     * @return The [TileDetailsViewModel] instance, or null if not implemented.
-     */
-    val detailsViewModel: TileDetailsViewModel?
-        get() = null
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
index 8c75cf0..7f475f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.TileDetailsViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
 import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
@@ -70,9 +71,7 @@
          * Creates [QSTileViewModelImpl] based on the interactors obtained from [QSTileComponent].
          * Reference of that [QSTileComponent] is then stored along the view model.
          */
-        fun create(
-            tileSpec: TileSpec,
-        ): QSTileViewModel {
+        fun create(tileSpec: TileSpec): QSTileViewModel {
             val config = qsTileConfigProvider.getConfig(tileSpec.spec)
             val component =
                 customTileComponentBuilder.qsTileConfigModule(QSTileConfigModule(config)).build()
@@ -90,6 +89,7 @@
                 backgroundDispatcher,
                 uiBackgroundDispatcher,
                 component.coroutineScope(),
+                /* tileDetailsViewModel= */ null,
             )
         }
     }
@@ -127,6 +127,7 @@
             userActionInteractor: QSTileUserActionInteractor<T>,
             tileDataInteractor: QSTileDataInteractor<T>,
             mapper: QSTileDataToStateMapper<T>,
+            tileDetailsViewModel: TileDetailsViewModel? = null,
         ): QSTileViewModelImpl<T> =
             QSTileViewModelImpl(
                 qsTileConfigProvider.getConfig(tileSpec.spec),
@@ -142,6 +143,7 @@
                 backgroundDispatcher,
                 uiBackgroundDispatcher,
                 coroutineScopeFactory.create(),
+                tileDetailsViewModel,
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index 30bf5b3..9bdec43 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -83,6 +83,7 @@
     private val backgroundDispatcher: CoroutineDispatcher,
     uiBackgroundDispatcher: CoroutineDispatcher,
     private val tileScope: CoroutineScope,
+    override val tileDetailsViewModel: TileDetailsViewModel? = null,
 ) : QSTileViewModel, Dumpable {
 
     private val users: MutableStateFlow<UserHandle> =
@@ -114,9 +115,6 @@
             .flowOn(backgroundDispatcher)
             .stateIn(tileScope, SharingStarted.WhileSubscribed(), true)
 
-    override val detailsViewModel: TileDetailsViewModel?
-        get() = userActionInteractor().detailsViewModel
-
     override fun forceUpdate() {
         tileScope.launch(context = backgroundDispatcher) { forceUpdates.emit(Unit) }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
index 0ed56f6..6709fd2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
@@ -16,9 +16,11 @@
 
 package com.android.systemui.qs.tiles.dialog
 
+import android.content.Intent
+import android.provider.Settings
 import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
 import com.android.systemui.statusbar.connectivity.AccessPointController
-import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 
@@ -27,10 +29,13 @@
 constructor(
     private val accessPointController: AccessPointController,
     val contentManagerFactory: InternetDetailsContentManager.Factory,
-    @Assisted private val onLongClick: () -> Unit,
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
 ) : TileDetailsViewModel() {
     override fun clickOnSettingsButton() {
-        onLongClick()
+        qsTileIntentUserActionHandler.handle(
+            /* expandable= */ null,
+            Intent(Settings.ACTION_WIFI_SETTINGS),
+        )
     }
 
     override fun getTitle(): String {
@@ -58,7 +63,7 @@
     }
 
     @AssistedFactory
-    interface Factory {
-        fun create(onLongClick: () -> Unit): InternetDetailsViewModel
+    fun interface Factory {
+        fun create(): InternetDetailsViewModel
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
index 0adc4131..8d4a24e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
@@ -400,6 +400,9 @@
 
         mInternetDialogTitle.setText(internetContent.mInternetDialogTitleString);
         mInternetDialogSubTitle.setText(internetContent.mInternetDialogSubTitle);
+        if (!internetContent.mIsWifiEnabled) {
+            setProgressBarVisible(false);
+        }
         mAirplaneModeButton.setVisibility(
                 internetContent.mIsAirplaneModeEnabled ? View.VISIBLE : View.GONE);
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
index 0ed46e7..5f692f2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles.impl.di
 
+import com.android.systemui.plugins.qs.TileDetailsViewModel
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index 8e48fe4..0431e36 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -18,13 +18,10 @@
 
 import android.content.Intent
 import android.provider.Settings
-import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.qs.TileDetailsViewModel
 import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
 import com.android.systemui.qs.tiles.base.interactor.QSTileInput
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
-import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
@@ -41,7 +38,6 @@
     private val internetDialogManager: InternetDialogManager,
     private val accessPointController: AccessPointController,
     private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
-    private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory,
 ) : QSTileUserActionInteractor<InternetTileModel> {
 
     override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit =
@@ -58,16 +54,12 @@
                     }
                 }
                 is QSTileUserAction.LongClick -> {
-                    handleLongClick(action.expandable)
+                    qsTileIntentUserActionHandler.handle(
+                        action.expandable,
+                        Intent(Settings.ACTION_WIFI_SETTINGS),
+                    )
                 }
                 else -> {}
             }
         }
-
-    override val detailsViewModel: TileDetailsViewModel =
-        internetDetailsViewModelFactory.create { handleLongClick(null) }
-
-    private fun handleLongClick(expandable: Expandable?) {
-        qsTileIntentUserActionHandler.handle(expandable, Intent(Settings.ACTION_WIFI_SETTINGS))
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index e8b9926..eeb8c85 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -39,7 +39,7 @@
     val isAvailable: StateFlow<Boolean>
 
     /** Specifies the [TileDetailsViewModel] for constructing the corresponding details view. */
-    val detailsViewModel: TileDetailsViewModel?
+    val tileDetailsViewModel: TileDetailsViewModel?
         get() = null
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 30d1f05..527c542 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -157,7 +157,7 @@
     }
 
     override fun getDetailsViewModel(): TileDetailsViewModel? {
-        return qsTileViewModel.detailsViewModel
+        return qsTileViewModel.tileDetailsViewModel
     }
 
     @Deprecated(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index 5042f1b..e998a73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -30,26 +30,25 @@
     @NotifInteractionLog private val buffer: LogBuffer
 ) {
     fun logInitialClick(
-        entry: NotificationEntry?,
+        entry: String?,
         index: Integer?,
         pendingIntent: PendingIntent
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry?.key
-            str2 = entry?.ranking?.channel?.id
+            str1 = entry
             str3 = pendingIntent.toString()
             int1 = index?.toInt() ?: Int.MIN_VALUE
         }, {
-            "ACTION CLICK $str1 (channel=$str2) for pending intent $str3 at index $int1"
+            "ACTION CLICK $str1 for pending intent $str3 at index $int1"
         })
     }
 
     fun logRemoteInputWasHandled(
-        entry: NotificationEntry?,
+        entry: String?,
         index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry?.key
+            str1 = entry
             int1 = index ?: Int.MIN_VALUE
         }, {
             "  [Action click] Triggered remote input (for $str1) at index $int1"
@@ -57,12 +56,12 @@
     }
 
     fun logStartingIntentWithDefaultHandler(
-        entry: NotificationEntry?,
+        entry: String?,
         pendingIntent: PendingIntent,
         index: Int?
     ) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry?.key
+            str1 = entry
             str2 = pendingIntent.toString()
             int1 = index ?: Int.MIN_VALUE
         }, {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 7dc2ae7..e44701d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -580,7 +580,8 @@
         /**
          * @see IStatusBar#immersiveModeChanged
          */
-        default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
+        default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+                int windowType) {}
 
         /**
          * @see IStatusBar#moveFocusedTaskToDesktop(int)
@@ -876,11 +877,13 @@
     }
 
     @Override
-    public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+    public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+            int windowType) {
         synchronized (mLock) {
             final SomeArgs args = SomeArgs.obtain();
             args.argi1 = rootDisplayAreaId;
             args.argi2 = isImmersiveMode ? 1 : 0;
+            args.argi3 = windowType;
             mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget();
         }
     }
@@ -2030,8 +2033,10 @@
                     args = (SomeArgs) msg.obj;
                     int rootDisplayAreaId = args.argi1;
                     boolean isImmersiveMode = args.argi2 != 0;
+                    int windowType = args.argi3;
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+                        mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode,
+                                windowType);
                     }
                     break;
                 case MSG_ENTER_DESKTOP: {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
index fed3f6e..97e62d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -23,6 +23,8 @@
 import static android.app.StatusBarManager.DISABLE_RECENT;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
 import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
 
@@ -208,7 +210,8 @@
     }
 
     @Override
-    public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+    public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+            int windowType) {
         mHandler.removeMessages(H.SHOW);
         if (isImmersiveMode) {
             if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed);
@@ -221,7 +224,9 @@
                     && mCanSystemBarsBeShownByUser
                     && !mNavBarEmpty
                     && !UserManager.isDeviceInDemoMode(mDisplayContext)
-                    && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
+                    && (mLockTaskState != LOCK_TASK_MODE_LOCKED)
+                    && windowType != TYPE_PRESENTATION
+                    && windowType != TYPE_PRIVATE_PRESENTATION) {
                 final Message msg = mHandler.obtainMessage(
                         H.SHOW);
                 msg.arg1 = rootDisplayAreaId;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 5b5058f..13737dbff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -560,10 +560,10 @@
         var entry: NotificationEntry? = null
         if (expandView is ExpandableNotificationRow) {
             entry = expandView.entry
-            entry.setUserExpanded(/* userExpanded= */ true, /* allowChildExpansion= */ true)
+            expandView.setUserExpanded(/* userExpanded= */ true, /* allowChildExpansion= */ true)
             // Indicate that the group expansion is changing at this time -- this way the group
             // and children backgrounds / divider animations will look correct.
-            entry.setGroupExpansionChanging(true)
+            expandView.isGroupExpansionChanging = true
             userId = entry.sbn.userId
         }
         var fullShadeNeedsBouncer =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index c0ee56b..0d34bdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -133,11 +133,14 @@
             Integer actionIndex = (Integer)
                     view.getTag(com.android.internal.R.id.notification_action_index_tag);
 
+            final ExpandableNotificationRow row = getNotificationRowForParent(view.getParent());
             final NotificationEntry entry = getNotificationForParent(view.getParent());
-            mLogger.logInitialClick(entry, actionIndex, pendingIntent);
+            mLogger.logInitialClick(
+                    row != null ? row.getLoggingKey() : null, actionIndex, pendingIntent);
 
             if (handleRemoteInput(view, pendingIntent)) {
-                mLogger.logRemoteInputWasHandled(entry, actionIndex);
+                mLogger.logRemoteInputWasHandled(
+                        row != null ? row.getLoggingKey() : null, actionIndex);
                 return true;
             }
 
@@ -157,7 +160,8 @@
             return mCallback.handleRemoteViewClick(view, pendingIntent,
                     action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> {
                     Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
-                    mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent, actionIndex);
+                    mLogger.logStartingIntentWithDefaultHandler(
+                            row != null ? row.getLoggingKey() : null, pendingIntent, actionIndex);
                     boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options);
                     if (started) releaseNotificationIfKeptForRemoteInputHistory(entry);
                     return started;
@@ -224,6 +228,16 @@
             return null;
         }
 
+        private @Nullable ExpandableNotificationRow getNotificationRowForParent(ViewParent parent) {
+            while (parent != null) {
+                if (parent instanceof ExpandableNotificationRow) {
+                    return ((ExpandableNotificationRow) parent);
+                }
+                parent = parent.getParent();
+            }
+            return null;
+        }
+
         private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
             if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
                 return true;
@@ -722,7 +736,7 @@
      *
      * @return on-click handler
      */
-    public RemoteViews.InteractionHandler getRemoteViewsOnClickHandler() {
+    public InteractionHandler getRemoteViewsOnClickHandler() {
         return mInteractionHandler;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index 10090283..48f0245 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.qs.tiles.NfcTile
 import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
 import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
 import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper
 import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
 import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
@@ -162,13 +163,15 @@
             factory: QSTileViewModelFactory.Static<AirplaneModeTileModel>,
             mapper: AirplaneModeMapper,
             stateInteractor: AirplaneModeTileDataInteractor,
-            userActionInteractor: AirplaneModeTileUserActionInteractor
+            userActionInteractor: AirplaneModeTileUserActionInteractor,
+            internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
         ): QSTileViewModel =
             factory.create(
                 TileSpec.create(AIRPLANE_MODE_TILE_SPEC),
                 userActionInteractor,
                 stateInteractor,
                 mapper,
+                internetDetailsViewModelFactory.create(),
             )
 
         @Provides
@@ -226,13 +229,15 @@
             factory: QSTileViewModelFactory.Static<InternetTileModel>,
             mapper: InternetTileMapper,
             stateInteractor: InternetTileDataInteractor,
-            userActionInteractor: InternetTileUserActionInteractor
+            userActionInteractor: InternetTileUserActionInteractor,
+            internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
         ): QSTileViewModel =
             factory.create(
                 TileSpec.create(INTERNET_TILE_SPEC),
                 userActionInteractor,
                 stateInteractor,
                 mapper,
+                internetDetailsViewModelFactory.create(),
             )
 
         @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
index 9bc5231..f52b924 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
@@ -74,7 +74,7 @@
             else
                 Notification.MessagingStyle.CONVERSATION_TYPE_LEGACY
         entry.ranking.conversationShortcutInfo?.let { shortcutInfo ->
-            logger.logAsyncTaskProgress(entry, "getting shortcut icon")
+            logger.logAsyncTaskProgress(entry.logKey, "getting shortcut icon")
             messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)
             shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index 6487d55..ccfb43e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -73,25 +73,25 @@
 
         final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
         final NotificationEntry entry = row.getEntry();
-        mLogger.logOnClick(entry);
+        mLogger.logOnClick(row.getLoggingKey());
 
         // Check if the notification is displaying the menu, if so slide notification back
         if (isMenuVisible(row)) {
-            mLogger.logMenuVisible(entry);
+            mLogger.logMenuVisible(row.getLoggingKey());
             row.animateResetTranslation();
             return;
         } else if (row.isChildInGroup() && isMenuVisible(row.getNotificationParent())) {
-            mLogger.logParentMenuVisible(entry);
+            mLogger.logParentMenuVisible(row.getLoggingKey());
             row.getNotificationParent().animateResetTranslation();
             return;
         } else if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
             // We never want to open the app directly if the user clicks in between
             // the notifications.
-            mLogger.logChildrenExpanded(entry);
+            mLogger.logChildrenExpanded(row.getLoggingKey());
             return;
         } else if (row.areGutsExposed()) {
             // ignore click if guts are exposed
-            mLogger.logGutsExposed(entry);
+            mLogger.logGutsExposed(row.getLoggingKey());
             return;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
index cea2b59..812c609 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -25,42 +25,41 @@
 class NotificationClickerLogger @Inject constructor(
     @NotifInteractionLog private val buffer: LogBuffer
 ) {
-    fun logOnClick(entry: NotificationEntry) {
+    fun logOnClick(entry: String) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.logKey
-            str2 = entry.ranking.channel.id
+            str1 = entry
         }, {
-            "CLICK $str1 (channel=$str2)"
+            "CLICK $str1"
         })
     }
 
-    fun logMenuVisible(entry: NotificationEntry) {
+    fun logMenuVisible(entry: String) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.logKey
+            str1 = entry
         }, {
             "Ignoring click on $str1; menu is visible"
         })
     }
 
-    fun logParentMenuVisible(entry: NotificationEntry) {
+    fun logParentMenuVisible(entry: String) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.logKey
+            str1 = entry
         }, {
             "Ignoring click on $str1; parent menu is visible"
         })
     }
 
-    fun logChildrenExpanded(entry: NotificationEntry) {
+    fun logChildrenExpanded(entry: String) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.logKey
+            str1 = entry
         }, {
             "Ignoring click on $str1; children are expanded"
         })
     }
 
-    fun logGutsExposed(entry: NotificationEntry) {
+    fun logGutsExposed(entry: String) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.logKey
+            str1 = entry
         }, {
             "Ignoring click on $str1; guts are exposed"
         })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
index ab40582..243a868 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
@@ -74,7 +74,7 @@
     }
 
     private val notificationEntry = notification.entry
-    private val notificationKey = notificationEntry.sbn.key
+    private val notificationKey = notification.key
 
     override val isLaunching: Boolean = true
 
@@ -175,7 +175,7 @@
         HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(notification, animate)
 
         headsUpManager.removeNotification(
-            row.entry.key,
+            row.key,
             true /* releaseImmediately */,
             animate,
             reason,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
index 31bcf2b..acbcab0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -94,7 +94,7 @@
 
     /** Get the notification key, reformatted for logging, for the (optional) row */
     public static String logKey(ExpandableNotificationRow row) {
-        return row == null ? "null" : logKey(row.getEntry());
+        return row == null ? "null" : row.getLoggingKey();
     }
 
     /** Removes newlines from the notification key to prettify apps that have these in the tag */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 35a2828..c79cae7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.os.Build;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
@@ -34,6 +35,10 @@
 
 import java.util.List;
 
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
 /**
  * Class to represent notifications bundled by classification.
  */
@@ -41,6 +46,9 @@
 
     private final BundleEntryAdapter mEntryAdapter;
 
+    // TODO(b/394483200): move NotificationEntry's implementation to PipelineEntry?
+    private final MutableStateFlow<Boolean> mSensitive = StateFlowKt.MutableStateFlow(false);
+
     // TODO (b/389839319): implement the row
     private ExpandableNotificationRow mRow;
 
@@ -97,20 +105,26 @@
             return true;
         }
 
+        @NonNull
         @Override
         public String getKey() {
             return mKey;
         }
 
         @Override
+        @Nullable
         public ExpandableNotificationRow getRow() {
             return mRow;
         }
 
-        @Nullable
         @Override
-        public EntryAdapter getGroupRoot() {
-            return this;
+        public boolean isGroupRoot() {
+            return true;
+        }
+
+        @Override
+        public StateFlow<Boolean> isSensitive() {
+            return BundleEntry.this.mSensitive;
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
index 6431cac..109ebe6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
@@ -24,6 +24,8 @@
 import com.android.systemui.statusbar.notification.icon.IconPack;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 
+import kotlinx.coroutines.flow.StateFlow;
+
 /**
  * Adapter interface for UI to get relevant info.
  */
@@ -51,15 +53,10 @@
     ExpandableNotificationRow getRow();
 
     /**
-     * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry
-     * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the
-     * BundleEntry will be returned. If the given notification is a group summary NotificationEntry,
-     * or a child of a group summary, the summary NotificationEntry will be returned, even if that
-     * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any
-     * group or bundle grouping, null will be returned.
+     * Whether this entry is the root of its collapsable 'group' - either a BundleEntry or a
+     * notification group summary
      */
-    @Nullable
-    EntryAdapter getGroupRoot();
+    boolean isGroupRoot();
 
     /**
      * @return whether the row can be removed with the 'Clear All' action
@@ -107,4 +104,9 @@
      * Retrieves the pack of icons associated with this entry
      */
     IconPack getIcons();
+
+    /**
+     * Returns whether the content of this entry is sensitive
+     */
+    StateFlow<Boolean> isSensitive();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 826329d..22de83e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -620,7 +620,7 @@
     }
 
     private boolean isDismissable(NotificationEntry entry) {
-        return mDismissibilityProvider.isDismissable(entry);
+        return mDismissibilityProvider.isDismissable(entry.getKey());
     }
 
     /**
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 e5b72d4..fb2a66c 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
@@ -297,20 +297,20 @@
             return NotificationEntry.this.getRow();
         }
 
-        @Nullable
         @Override
-        public EntryAdapter getGroupRoot() {
-            // TODO (b/395857098): for backwards compatibility this will return null if called
-            // on a group summary that's not in a bundles, but it should return itself.
+        public boolean isGroupRoot() {
             if (isTopLevelEntry() || getParent() == null) {
-                return null;
+                return false;
             }
             if (NotificationEntry.this.getParent() instanceof GroupEntry parentGroupEntry) {
-                if (parentGroupEntry.getSummary() != null) {
-                    return parentGroupEntry.getSummary().mEntryAdapter;
-                }
+                return parentGroupEntry.getSummary() == NotificationEntry.this;
             }
-            return null;
+            return false;
+        }
+
+        @Override
+        public StateFlow<Boolean> isSensitive() {
+            return NotificationEntry.this.isSensitive();
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProvider.kt
index 53f2366..75bf38f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProvider.kt
@@ -20,6 +20,6 @@
 
 /** Keeps track of the dismissibility of Notifications currently handed over to the view layer. */
 interface NotificationDismissibilityProvider {
-    /** @return true if the given {NotificationEntry} can currently be dismissed by the user */
-    fun isDismissable(entry: NotificationEntry): Boolean
+    /** @return true if the given entry's key can currently be dismissed by the user */
+    fun isDismissable(entryKey: String): Boolean
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
index 9326d33..740442a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
@@ -39,8 +39,8 @@
     var nonDismissableEntryKeys = setOf<String>()
         private set
 
-    override fun isDismissable(entry: NotificationEntry): Boolean {
-        return entry.key !in nonDismissableEntryKeys
+    override fun isDismissable(entryKey: String): Boolean {
+        return entryKey !in nonDismissableEntryKeys
     }
 
     @Synchronized
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index b179a69..c5ae875 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -29,6 +29,7 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
 
 import java.io.PrintWriter;
@@ -162,41 +163,38 @@
     @Override
     public boolean isGroupExpanded(EntryAdapter entry) {
         NotificationBundleUi.assertInNewMode();
-        return mExpandedCollections.contains(mGroupMembershipManager.getGroupRoot(entry));
+        ExpandableNotificationRow parent = entry.getRow().getNotificationParent();
+        return mExpandedCollections.contains(entry)
+                || (parent != null && mExpandedCollections.contains(parent.getEntryAdapter()));
     }
 
     @Override
-    public void setGroupExpanded(EntryAdapter entry, boolean expanded) {
+    public void setGroupExpanded(EntryAdapter groupRoot, boolean expanded) {
         NotificationBundleUi.assertInNewMode();
-        EntryAdapter groupParent = mGroupMembershipManager.getGroupRoot(entry);
-        if (!entry.isAttached()) {
+        if (!groupRoot.isAttached()) {
             if (expanded) {
                 Log.wtf(TAG, "Cannot expand group that is not attached");
-            } else {
-                // The entry is no longer attached, but we still want to make sure we don't have
-                // a stale expansion state.
-                groupParent = entry;
             }
         }
 
         boolean changed;
         if (expanded) {
-            changed = mExpandedCollections.add(groupParent);
+            changed = mExpandedCollections.add(groupRoot);
         } else {
-            changed = mExpandedCollections.remove(groupParent);
+            changed = mExpandedCollections.remove(groupRoot);
         }
 
         // Only notify listeners if something changed.
         if (changed) {
-            sendOnGroupExpandedChange(entry, expanded);
+            sendOnGroupExpandedChange(groupRoot, expanded);
         }
     }
 
     @Override
-    public boolean toggleGroupExpansion(EntryAdapter entry) {
+    public boolean toggleGroupExpansion(EntryAdapter groupRoot) {
         NotificationBundleUi.assertInNewMode();
-        setGroupExpanded(entry, !isGroupExpanded(entry));
-        return isGroupExpanded(entry);
+        setGroupExpanded(groupRoot, !isGroupExpanded(groupRoot));
+        return isGroupExpanded(groupRoot);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
index 3edbfaf..86aa4a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
@@ -51,17 +51,6 @@
     NotificationEntry getGroupSummary(@NonNull NotificationEntry entry);
 
     /**
-     * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry
-     * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the
-     * BundleEntry will be returned. If the given notification is a group summary NotificationEntry,
-     * or a child of a group summary, the summary NotificationEntry will be returned, even if that
-     * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any
-     * group or bundle grouping, null will be returned.
-     */
-    @Nullable
-    EntryAdapter getGroupRoot(@NonNull EntryAdapter entry);
-
-    /**
      * @return whether a given notification is a child in a group
      */
     boolean isChildInGroup(@NonNull NotificationEntry entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
index a1a23e3..aec0d70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
@@ -60,7 +60,7 @@
     @Override
     public boolean isGroupRoot(@NonNull EntryAdapter entry) {
         NotificationBundleUi.assertInNewMode();
-        return entry == entry.getGroupRoot();
+        return entry.isGroupRoot();
     }
 
     @Nullable
@@ -76,13 +76,6 @@
         return null;
     }
 
-    @Nullable
-    @Override
-    public EntryAdapter getGroupRoot(@NonNull EntryAdapter entry) {
-        NotificationBundleUi.assertInNewMode();
-        return entry.getGroupRoot();
-    }
-
     @Override
     public boolean isChildInGroup(@NonNull NotificationEntry entry) {
         NotificationBundleUi.assertInLegacyMode();
@@ -94,7 +87,7 @@
     public boolean isChildInGroup(@NonNull EntryAdapter entry) {
         NotificationBundleUi.assertInNewMode();
         // An entry is a child if it's not a group root or top level entry, but it is attached.
-        return entry.isAttached() && entry != getGroupRoot(entry) && !entry.isTopLevelEntry();
+        return entry.isAttached() && !entry.isGroupRoot() && !entry.isTopLevelEntry();
     }
 
     @Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
index 5f7acea..9728fcf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
@@ -19,7 +19,9 @@
 import android.graphics.Region
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.EntryAdapter
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import dagger.Binds
 import dagger.Module
 import java.io.PrintWriter
@@ -153,6 +155,12 @@
     fun setAnimationStateHandler(handler: AnimationStateHandler)
 
     /**
+    * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until
+    * it's collapsed again.
+    */
+    fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean)
+
+    /**
      * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until
      * it's collapsed again.
      */
@@ -310,6 +318,8 @@
 
     override fun setAnimationStateHandler(handler: AnimationStateHandler) {}
 
+    override fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean) {}
+
     override fun setExpanded(entry: NotificationEntry, expanded: Boolean) {}
 
     override fun setGutsShown(entry: NotificationEntry, gutsShown: Boolean) {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index acb27a4..7c5f3b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -878,10 +878,8 @@
             ExpandableNotificationRow topRow = topEntry.getRow();
             if (topEntry.rowIsChildInGroup()) {
                 if (NotificationBundleUi.isEnabled()) {
-                    final EntryAdapter adapter = mGroupMembershipManager.getGroupRoot(
-                            topRow.getEntryAdapter());
-                    if (adapter != null) {
-                        topRow = adapter.getRow();
+                    if (topRow.getNotificationParent() != null) {
+                        topRow = topRow.getNotificationParent();
                     }
                 } else {
                     final NotificationEntry groupSummary =
@@ -1093,7 +1091,23 @@
      * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
      * until it's collapsed again.
      */
+    @Override
+    public void setExpanded(@NonNull String entryKey, @NonNull ExpandableNotificationRow row,
+            boolean expanded) {
+        NotificationBundleUi.assertInNewMode();
+        HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey);
+        if (headsUpEntry != null && row.getPinnedStatus().isPinned()) {
+            headsUpEntry.setExpanded(expanded);
+        }
+    }
+
+    /**
+     * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
+     * until it's collapsed again.
+     */
+    @Override
     public void setExpanded(@NonNull NotificationEntry entry, boolean expanded) {
+        NotificationBundleUi.assertInLegacyMode();
         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
         if (headsUpEntry != null && entry.isRowPinned()) {
             headsUpEntry.setExpanded(expanded);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
index 47e725c..95f07c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
@@ -141,7 +141,7 @@
                 if (mPickedChild != null && mTouchingHeadsUpView) {
                     // We may swallow this click if the heads up just came in.
                     if (mHeadsUpManager.shouldSwallowClick(
-                            mPickedChild.getEntry().getSbn().getKey())) {
+                            mPickedChild.getKey())) {
                         endMotion();
                         return true;
                     }
@@ -209,7 +209,7 @@
                     if (mPickedChild != null && mTouchingHeadsUpView) {
                         // We may swallow this click if the heads up just came in.
                         if (mHeadsUpManager.shouldSwallowClick(
-                                mPickedChild.getEntry().getSbn().getKey())) {
+                                mPickedChild.getKey())) {
                             endMotion();
                             setTrackingHeadsUp(false);
                             return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpUtil.java
index 40da232..e1cbdac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpUtil.java
@@ -76,12 +76,7 @@
         }
 
         final ExpandableNotificationRow row = (ExpandableNotificationRow) view;
-        final NotificationEntry entry = row.getEntry();
-        if (entry == null) {
-            return "(null entry)";
-        }
-
-        final String key = entry.getKey();
+        final String key = row.getKey();
         if (key == null) {
             return "(null key)";
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
index 0e1f66f..48095cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
@@ -41,7 +41,7 @@
             TAG_ROUNDNESS,
             INFO,
             {
-                str1 = (view as? ExpandableNotificationRow)?.entry?.key
+                str1 = (view as? ExpandableNotificationRow)?.key
                 bool1 = isFirstInSection
                 bool2 = isLastInSection
                 bool3 = topChanged
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
index 6bfc9f0..4bd6053 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
@@ -21,7 +21,6 @@
 import android.app.NotificationChannel
 import android.app.NotificationChannel.DEFAULT_CHANNEL_ID
 import android.app.NotificationChannelGroup
-import android.app.NotificationManager.IMPORTANCE_NONE
 import android.app.NotificationManager.Importance
 import android.content.Context
 import android.graphics.Color
@@ -40,7 +39,7 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.res.R
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
 import javax.inject.Inject
 
 private const val TAG = "ChannelDialogController"
@@ -59,9 +58,9 @@
  */
 @SysUISingleton
 class ChannelEditorDialogController @Inject constructor(
-    @ShadeDisplayAware private val context: Context,
+    private val shadeDialogContextInteractor: ShadeDialogContextInteractor,
     private val noMan: INotificationManager,
-    private val dialogBuilder: ChannelEditorDialog.Builder
+    private val dialogBuilder: ChannelEditorDialog.Builder,
 ) {
 
     private var prepared = false
@@ -272,7 +271,7 @@
     }
 
     private fun initDialog() {
-        dialogBuilder.setContext(context)
+        dialogBuilder.setContext(shadeDialogContextInteractor.context)
         dialog = dialogBuilder.build()
 
         dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 108c060..6134d1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -270,6 +270,7 @@
     private NotificationContentView[] mLayouts;
     private ExpandableNotificationRowLogger mLogger;
     private String mLoggingKey;
+    private String mKey;
     private NotificationGuts mGuts;
     private NotificationEntry mEntry;
     private EntryAdapter mEntryAdapter;
@@ -404,17 +405,25 @@
                 : mGroupMembershipManager.isGroupSummary(mEntry);
         if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) {
             mGroupExpansionChanging = true;
-            final boolean wasExpanded = NotificationBundleUi.isEnabled()
-                    ? mGroupExpansionManager.isGroupExpanded(mEntryAdapter)
-                    : mGroupExpansionManager.isGroupExpanded(mEntry);
-            boolean nowExpanded = NotificationBundleUi.isEnabled()
-                    ? mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter)
-                    : mGroupExpansionManager.toggleGroupExpansion(mEntry);
-            mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
-            if (shouldLogExpandClickMetric) {
-                mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded);
+            if (NotificationBundleUi.isEnabled()) {
+                final boolean wasExpanded =  mGroupExpansionManager.isGroupExpanded(mEntryAdapter);
+                boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter);
+                mOnExpandClickListener.onExpandClicked(this, mEntryAdapter, nowExpanded);
+                if (shouldLogExpandClickMetric) {
+                    mMetricsLogger.action(
+                            MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded);
+                }
+                onExpansionChanged(true /* userAction */, wasExpanded);
+            } else {
+                final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
+                boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntry);
+                mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
+                if (shouldLogExpandClickMetric) {
+                    mMetricsLogger.action(
+                            MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded);
+                }
+                onExpansionChanged(true /* userAction */, wasExpanded);
             }
-            onExpansionChanged(true /* userAction */, wasExpanded);
         } else if (mEnableNonGroupedNotificationExpand) {
             if (v.isAccessibilityFocused()) {
                 mPrivateLayout.setFocusOnVisibilityChange();
@@ -435,7 +444,11 @@
             }
 
             notifyHeightChanged(/* needsAnimation= */ true);
-            mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
+            if (NotificationBundleUi.isEnabled()) {
+                mOnExpandClickListener.onExpandClicked(this, mEntryAdapter, nowExpanded);
+            } else {
+                mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
+            }
             if (shouldLogExpandClickMetric) {
                 mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_EXPANDER, nowExpanded);
             }
@@ -535,6 +548,14 @@
         return mLoggingKey;
     }
 
+    public String getKey() {
+        if (NotificationBundleUi.isEnabled()) {
+            return mKey;
+        } else {
+            return mEntry.getKey();
+        }
+    }
+
     /**
      * Sets animations running in the layouts of this row, including public, private, and children.
      *
@@ -1224,7 +1245,7 @@
      */
     public void collectVisibleLocations(Map<String, Integer> locationsMap) {
         if (getVisibility() == View.VISIBLE) {
-            locationsMap.put(getEntry().getKey(), getViewState().location);
+            locationsMap.put(getKey(), getViewState().location);
             if (mChildrenContainer != null) {
                 List<ExpandableNotificationRow> children = mChildrenContainer.getAttachedChildren();
                 for (int i = 0; i < children.size(); i++) {
@@ -2136,6 +2157,7 @@
             mMenuRow.setAppName(mAppName);
         }
         mLogger = logger;
+        mKey = notificationKey;
         mLoggingKey = logKey(notificationKey);
         mBypassController = bypassController;
         mGroupMembershipManager = groupMembershipManager;
@@ -2946,7 +2968,9 @@
                 && !mChildrenContainer.showingAsLowPriority()) {
             final boolean wasExpanded = isGroupExpanded();
             if (NotificationBundleUi.isEnabled()) {
-                mGroupExpansionManager.setGroupExpanded(mEntryAdapter, userExpanded);
+                if (mEntryAdapter.isGroupRoot()) {
+                    mGroupExpansionManager.setGroupExpanded(mEntryAdapter, userExpanded);
+                }
             } else {
                 mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded);
             }
@@ -3416,7 +3440,7 @@
     }
 
     private boolean canEntryBeDismissed() {
-        return mDismissibilityProvider.isDismissable(mEntry);
+        return mDismissibilityProvider.isDismissable(getKey());
     }
 
     /**
@@ -3440,9 +3464,9 @@
     public void makeActionsVisibile() {
         setUserExpanded(true, true);
         if (isChildInGroup()) {
-            if (NotificationBundleUi.isEnabled()) {
-                mGroupExpansionManager.setGroupExpanded(mEntryAdapter, true);
-            } else {
+            if (!NotificationBundleUi.isEnabled()) {
+                // this is only called if row.getParent() instanceof NotificationStackScrollLayout,
+                // so there is never a group to expand
                 mGroupExpansionManager.setGroupExpanded(mEntry, true);
             }
         }
@@ -4023,6 +4047,9 @@
 
     public interface OnExpandClickListener {
         void onExpandClicked(NotificationEntry clickedEntry, View clickedView, boolean nowExpanded);
+
+        void onExpandClicked(ExpandableNotificationRow row, EntryAdapter clickedEntry,
+                boolean nowExpanded);
     }
 
     @Override
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 4b2b168..7444679 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
@@ -56,6 +56,7 @@
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
+import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
@@ -141,11 +142,11 @@
             // We don't want to reinflate anything for removed notifications. Otherwise views might
             // be readded to the stack, leading to leaks. This may happen with low-priority groups
             // where the removal of already removed children can lead to a reinflation.
-            mLogger.logNotBindingRowWasRemoved(entry);
+            mLogger.logNotBindingRowWasRemoved(row.getLoggingKey());
             return;
         }
 
-        mLogger.logBinding(entry, contentToBind);
+        mLogger.logBinding(row.getLoggingKey(), contentToBind);
 
         StatusBarNotification sbn = entry.getSbn();
 
@@ -283,7 +284,7 @@
             @NonNull ExpandableNotificationRow row) {
         final boolean abortedTask = entry.abortTask();
         if (abortedTask) {
-            mLogger.logCancelBindAbortedTask(entry);
+            mLogger.logCancelBindAbortedTask(row.getLoggingKey());
         }
         return abortedTask;
     }
@@ -293,7 +294,7 @@
             @NonNull NotificationEntry entry,
             @NonNull ExpandableNotificationRow row,
             @InflationFlag int contentToUnbind) {
-        mLogger.logUnbinding(entry, contentToUnbind);
+        mLogger.logUnbinding(row.getLoggingKey(), contentToUnbind);
         int curFlag = 1;
         while (contentToUnbind != 0) {
             if ((contentToUnbind & curFlag) != 0) {
@@ -410,18 +411,19 @@
                 && result.newExpandedView != null;
         boolean inflateHeadsUp = (reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0
                 && result.newHeadsUpView != null;
+        String logKey = NotificationUtils.logKey(entry);
         if (inflateContracted || inflateExpanded || inflateHeadsUp) {
-            logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state");
+            logger.logAsyncTaskProgress(logKey, "inflating contracted smart reply state");
             result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry);
         }
         if (inflateExpanded) {
-            logger.logAsyncTaskProgress(entry, "inflating expanded smart reply state");
+            logger.logAsyncTaskProgress(logKey, "inflating expanded smart reply state");
             result.expandedInflatedSmartReplies = inflater.inflateSmartReplyViewHolder(
                     context, packageContext, entry, previousSmartReplyState,
                     result.inflatedSmartReplyState);
         }
         if (inflateHeadsUp) {
-            logger.logAsyncTaskProgress(entry, "inflating heads up smart reply state");
+            logger.logAsyncTaskProgress(logKey, "inflating heads up smart reply state");
             result.headsUpInflatedSmartReplies = inflater.inflateSmartReplyViewHolder(
                     context, packageContext, entry, previousSmartReplyState,
                     result.inflatedSmartReplyState);
@@ -438,23 +440,21 @@
             NotificationRowContentBinderLogger logger) {
         return TraceUtils.trace("NotificationContentInflater.createRemoteViews", () -> {
             InflationProgress result = new InflationProgress();
-            final NotificationEntry entryForLogging = row.getEntry();
-
             // create an image inflater
             result.mRowImageInflater = RowImageInflater.newInstance(row.mImageModelIndex);
 
             if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
-                logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
+                logger.logAsyncTaskProgress(row.getLoggingKey(), "creating contracted remote view");
                 result.newContentView = createContentView(builder, bindParams.isMinimized);
             }
 
             if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
-                logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
+                logger.logAsyncTaskProgress(row.getLoggingKey(), "creating expanded remote view");
                 result.newExpandedView = createExpandedView(builder, bindParams.isMinimized);
             }
 
             if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
-                logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view");
+                logger.logAsyncTaskProgress(row.getLoggingKey(), "creating heads up remote view");
                 final boolean isHeadsUpCompact = headsUpStyleProvider.shouldApplyCompactStyle();
                 if (isHeadsUpCompact) {
                     result.newHeadsUpView = builder.createCompactHeadsUpContentView();
@@ -464,7 +464,7 @@
             }
 
             if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
-                logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
+                logger.logAsyncTaskProgress(row.getLoggingKey(), "creating public remote view");
                 if (LockscreenOtpRedaction.isEnabled()
                         && bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
                     result.newPublicView = createSensitiveContentMessageNotification(
@@ -477,13 +477,13 @@
 
             if (AsyncGroupHeaderViewInflation.isEnabled()) {
                 if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
-                    logger.logAsyncTaskProgress(entryForLogging,
+                    logger.logAsyncTaskProgress(row.getLoggingKey(),
                             "creating group summary remote view");
                     result.mNewGroupHeaderView = builder.makeNotificationGroupHeader();
                 }
 
                 if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
-                    logger.logAsyncTaskProgress(entryForLogging,
+                    logger.logAsyncTaskProgress(row.getLoggingKey(),
                             "creating low-priority group summary remote view");
                     result.mNewMinimizedGroupHeaderView =
                             builder.makeLowPriorityContentView(true /* useRegularSubtext */);
@@ -577,6 +577,7 @@
             @Nullable InflationCallback callback,
             NotificationRowContentBinderLogger logger) {
         Trace.beginAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
+        String logKey = NotificationUtils.logKey(entry);
 
         NotificationContentView privateLayout = row.getPrivateLayout();
         NotificationContentView publicLayout = row.getPublicLayout();
@@ -590,7 +591,7 @@
             ApplyCallback applyCallback = new ApplyCallback() {
                 @Override
                 public void setResultView(View v) {
-                    logger.logAsyncTaskProgress(entry, "contracted view applied");
+                    logger.logAsyncTaskProgress(logKey, "contracted view applied");
                     result.inflatedContentView = v;
                 }
 
@@ -599,7 +600,7 @@
                     return result.newContentView;
                 }
             };
-            logger.logAsyncTaskProgress(entry, "applying contracted view");
+            logger.logAsyncTaskProgress(logKey, "applying contracted view");
             applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
                     reInflateFlags, flag,
                     remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
@@ -618,7 +619,7 @@
                 ApplyCallback applyCallback = new ApplyCallback() {
                     @Override
                     public void setResultView(View v) {
-                        logger.logAsyncTaskProgress(entry, "expanded view applied");
+                        logger.logAsyncTaskProgress(logKey, "expanded view applied");
                         result.inflatedExpandedView = v;
                     }
 
@@ -627,7 +628,7 @@
                         return result.newExpandedView;
                     }
                 };
-                logger.logAsyncTaskProgress(entry, "applying expanded view");
+                logger.logAsyncTaskProgress(logKey, "applying expanded view");
                 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
                         reInflateFlags,
                         flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
@@ -646,7 +647,7 @@
                 ApplyCallback applyCallback = new ApplyCallback() {
                     @Override
                     public void setResultView(View v) {
-                        logger.logAsyncTaskProgress(entry, "heads up view applied");
+                        logger.logAsyncTaskProgress(logKey, "heads up view applied");
                         result.inflatedHeadsUpView = v;
                     }
 
@@ -655,7 +656,7 @@
                         return result.newHeadsUpView;
                     }
                 };
-                logger.logAsyncTaskProgress(entry, "applying heads up view");
+                logger.logAsyncTaskProgress(logKey, "applying heads up view");
                 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
                         result, reInflateFlags,
                         flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
@@ -673,7 +674,7 @@
             ApplyCallback applyCallback = new ApplyCallback() {
                 @Override
                 public void setResultView(View v) {
-                    logger.logAsyncTaskProgress(entry, "public view applied");
+                    logger.logAsyncTaskProgress(logKey, "public view applied");
                     result.inflatedPublicView = v;
                 }
 
@@ -682,7 +683,7 @@
                     return result.newPublicView;
                 }
             };
-            logger.logAsyncTaskProgress(entry, "applying public view");
+            logger.logAsyncTaskProgress(logKey, "applying public view");
             applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
                     result, reInflateFlags, flag,
                     remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
@@ -702,7 +703,7 @@
                 ApplyCallback applyCallback = new ApplyCallback() {
                     @Override
                     public void setResultView(View v) {
-                        logger.logAsyncTaskProgress(entry, "group header view applied");
+                        logger.logAsyncTaskProgress(logKey, "group header view applied");
                         result.mInflatedGroupHeaderView = (NotificationHeaderView) v;
                     }
 
@@ -711,7 +712,7 @@
                         return result.mNewGroupHeaderView;
                     }
                 };
-                logger.logAsyncTaskProgress(entry, "applying group header view");
+                logger.logAsyncTaskProgress(logKey, "applying group header view");
                 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
                         result, reInflateFlags,
                         /* inflationId = */ FLAG_GROUP_SUMMARY_HEADER,
@@ -731,7 +732,7 @@
                 ApplyCallback applyCallback = new ApplyCallback() {
                     @Override
                     public void setResultView(View v) {
-                        logger.logAsyncTaskProgress(entry,
+                        logger.logAsyncTaskProgress(logKey,
                                 "low-priority group header view applied");
                         result.mInflatedMinimizedGroupHeaderView = (NotificationHeaderView) v;
                     }
@@ -741,7 +742,7 @@
                         return result.mNewMinimizedGroupHeaderView;
                     }
                 };
-                logger.logAsyncTaskProgress(entry, "applying low priority group header view");
+                logger.logAsyncTaskProgress(logKey, "applying low priority group header view");
                 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
                         result, reInflateFlags,
                         /* inflationId = */ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
@@ -760,7 +761,7 @@
         CancellationSignal cancellationSignal = new CancellationSignal();
         cancellationSignal.setOnCancelListener(
                 () -> {
-                    logger.logAsyncTaskProgress(entry, "apply cancelled");
+                    logger.logAsyncTaskProgress(logKey, "apply cancelled");
                     Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
                     runningInflations.values().forEach(CancellationSignal::cancel);
                 });
@@ -807,7 +808,7 @@
                     existingWrapper.onReinflated();
                 }
             } catch (Exception e) {
-                handleInflationError(runningInflations, e, row.getEntry(), callback, logger,
+                handleInflationError(runningInflations, e, row, callback, logger,
                         "applying view synchronously");
                 // Add a running inflation to make sure we don't trigger callbacks.
                 // Safe to do because only happens in tests.
@@ -829,7 +830,7 @@
                 String invalidReason = isValidView(v, entry, row.getResources());
                 if (invalidReason != null) {
                     handleInflationError(runningInflations, new InflationException(invalidReason),
-                            row.getEntry(), callback, logger, "applied invalid view");
+                            row, callback, logger, "applied invalid view");
                     runningInflations.remove(inflationId);
                     return;
                 }
@@ -866,7 +867,7 @@
                     onViewApplied(newView);
                 } catch (Exception anotherException) {
                     runningInflations.remove(inflationId);
-                    handleInflationError(runningInflations, e, row.getEntry(),
+                    handleInflationError(runningInflations, e, row,
                             callback, logger, "applying view");
                 }
             }
@@ -962,13 +963,13 @@
 
     private static void handleInflationError(
             HashMap<Integer, CancellationSignal> runningInflations, Exception e,
-            NotificationEntry notification, @Nullable InflationCallback callback,
+            ExpandableNotificationRow row, @Nullable InflationCallback callback,
             NotificationRowContentBinderLogger logger, String logContext) {
         Assert.isMainThread();
-        logger.logAsyncTaskException(notification, logContext, e);
+        logger.logAsyncTaskException(row.getLoggingKey(), logContext, e);
         runningInflations.values().forEach(CancellationSignal::cancel);
         if (callback != null) {
-            callback.handleInflationException(notification, e);
+            callback.handleInflationException(row.getEntry(), e);
         }
     }
 
@@ -989,7 +990,7 @@
         }
         NotificationContentView privateLayout = row.getPrivateLayout();
         NotificationContentView publicLayout = row.getPublicLayout();
-        logger.logAsyncTaskProgress(entry, "finishing");
+        logger.logAsyncTaskProgress(NotificationUtils.logKey(entry), "finishing");
 
         // Put the new image index on the row
         row.mImageModelIndex = result.mRowImageInflater.getNewImageIndex();
@@ -1284,7 +1285,8 @@
                             return doInBackgroundInternal();
                         } catch (Exception e) {
                             mError = e;
-                            mLogger.logAsyncTaskException(mEntry, "inflating", e);
+                            mLogger.logAsyncTaskException(
+                                    NotificationUtils.logKey(mEntry), "inflating", e);
                             return null;
                         }
                     });
@@ -1311,12 +1313,12 @@
             InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
                     recoveredBuilder, mBindParams, mContext, packageContext, mRow,
                     mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, mLogger);
-
-            mLogger.logAsyncTaskProgress(mEntry,
+            String logKey = NotificationUtils.logKey(mEntry);
+            mLogger.logAsyncTaskProgress(logKey,
                     "getting existing smart reply state (on wrong thread!)");
             InflatedSmartReplyState previousSmartReplyState =
                     mRow.getExistingSmartReplyState();
-            mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
+            mLogger.logAsyncTaskProgress(logKey, "inflating smart reply views");
             InflationProgress result = inflateSmartReplyViews(
                     /* result = */ inflationProgress,
                     mReInflateFlags,
@@ -1379,26 +1381,26 @@
             }
 
             if (PromotedNotificationContentModel.featureFlagEnabled()) {
-                mLogger.logAsyncTaskProgress(mEntry, "extracting promoted notification content");
+                mLogger.logAsyncTaskProgress(logKey, "extracting promoted notification content");
                 final ImageModelProvider imageModelProvider =
                         result.mRowImageInflater.useForContentModel();
                 final PromotedNotificationContentModel promotedContent =
                         mPromotedNotificationContentExtractor.extractContent(mEntry,
                                 recoveredBuilder, imageModelProvider);
-                mLogger.logAsyncTaskProgress(mEntry, "extracted promoted notification content: "
+                mLogger.logAsyncTaskProgress(logKey, "extracted promoted notification content: "
                         + promotedContent);
 
                 result.mPromotedContent = promotedContent;
             }
 
-            mLogger.logAsyncTaskProgress(mEntry, "loading RON images");
+            mLogger.logAsyncTaskProgress(logKey, "loading RON images");
             inflationProgress.mRowImageInflater.loadImagesSynchronously(packageContext);
 
-            mLogger.logAsyncTaskProgress(mEntry,
+            mLogger.logAsyncTaskProgress(logKey,
                     "getting row image resolver (on wrong thread!)");
             final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
             // wait for image resolver to finish preloading
-            mLogger.logAsyncTaskProgress(mEntry, "waiting for preloaded images");
+            mLogger.logAsyncTaskProgress(logKey, "waiting for preloaded images");
             imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
 
             return result;
@@ -1449,13 +1451,14 @@
 
         @Override
         public void abort() {
-            mLogger.logAsyncTaskProgress(mEntry, "cancelling inflate");
+            String logKey = NotificationUtils.logKey(mEntry);
+            mLogger.logAsyncTaskProgress(logKey, "cancelling inflate");
             cancel(true /* mayInterruptIfRunning */);
             if (mCancellationSignal != null) {
-                mLogger.logAsyncTaskProgress(mEntry, "cancelling apply");
+                mLogger.logAsyncTaskProgress(logKey, "cancelling apply");
                 mCancellationSignal.cancel();
             }
-            mLogger.logAsyncTaskProgress(mEntry, "aborted");
+            mLogger.logAsyncTaskProgress(logKey, "aborted");
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index ab382df..e89a76f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.notification.row;
 
-import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
+import static android.app.Flags.notificationsRedesignTemplates;
 import static android.view.HapticFeedbackConstants.CLOCK_TICK;
 
 import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
@@ -706,8 +706,11 @@
     static NotificationMenuItem createInfoItem(Context context) {
         Resources res = context.getResources();
         String infoDescription = res.getString(R.string.notification_menu_gear_description);
+        int layoutId = notificationsRedesignTemplates()
+                ? R.layout.notification_2025_info
+                : R.layout.notification_info;
         NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
-                R.layout.notification_info, null, false);
+                layoutId, null, false);
         return new NotificationMenuItem(context, infoDescription, infoContent,
                 R.drawable.ic_settings);
     }
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 b9a3594..c930dd8 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
@@ -51,6 +51,7 @@
 import com.android.systemui.statusbar.notification.NmSummarizationUiFlag
 import com.android.systemui.statusbar.notification.collection.EntryAdapter
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+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.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
@@ -125,10 +126,10 @@
             // We don't want to reinflate anything for removed notifications. Otherwise views might
             // be readded to the stack, leading to leaks. This may happen with low-priority groups
             // where the removal of already removed children can lead to a reinflation.
-            logger.logNotBindingRowWasRemoved(entry)
+            logger.logNotBindingRowWasRemoved(row.loggingKey)
             return
         }
-        logger.logBinding(entry, contentToBind)
+        logger.logBinding(row.loggingKey, contentToBind)
         val sbn: StatusBarNotification = entry.sbn
 
         // To check if the notification has inline image and preload inline image if necessary.
@@ -245,7 +246,7 @@
     override fun cancelBind(entry: NotificationEntry, row: ExpandableNotificationRow): Boolean {
         val abortedTask: Boolean = entry.abortTask()
         if (abortedTask) {
-            logger.logCancelBindAbortedTask(entry)
+            logger.logCancelBindAbortedTask(row.loggingKey)
         }
         return abortedTask
     }
@@ -256,7 +257,7 @@
         row: ExpandableNotificationRow,
         @InflationFlag contentToUnbind: Int,
     ) {
-        logger.logUnbinding(entry, contentToUnbind)
+        logger.logUnbinding(row.loggingKey, contentToUnbind)
         var curFlag = 1
         var contentLeftToUnbind = contentToUnbind
         while (contentLeftToUnbind != 0) {
@@ -419,7 +420,7 @@
                 try {
                     return@trace Result.success(doInBackgroundInternal())
                 } catch (e: Exception) {
-                    logger.logAsyncTaskException(entry, "inflating", e)
+                    logger.logAsyncTaskException(entry.logKey, "inflating", e)
                     return@trace Result.failure(e)
                 }
             }
@@ -451,11 +452,11 @@
                     logger = logger,
                 )
             logger.logAsyncTaskProgress(
-                entry,
+                row.loggingKey,
                 "getting existing smart reply state (on wrong thread!)",
             )
             val previousSmartReplyState: InflatedSmartReplyState? = row.existingSmartReplyState
-            logger.logAsyncTaskProgress(entry, "inflating smart reply views")
+            logger.logAsyncTaskProgress(entry.logKey, "inflating smart reply views")
             inflateSmartReplyViews(
                 /* result = */ inflationProgress,
                 reInflateFlags,
@@ -467,7 +468,7 @@
                 logger,
             )
             if (AsyncHybridViewInflation.isEnabled) {
-                logger.logAsyncTaskProgress(entry, "inflating single line view")
+                logger.logAsyncTaskProgress(entry.logKey, "inflating single line view")
                 inflationProgress.inflatedSingleLineView =
                     inflationProgress.contentModel.singleLineViewModel?.let {
                         SingleLineViewInflater.inflatePrivateSingleLineView(
@@ -481,7 +482,7 @@
             }
 
             if (LockscreenOtpRedaction.isEnabled) {
-                logger.logAsyncTaskProgress(entry, "inflating public single line view")
+                logger.logAsyncTaskProgress(entry.logKey, "inflating public single line view")
                 inflationProgress.inflatedPublicSingleLineView =
                     inflationProgress.contentModel.publicSingleLineViewModel?.let { viewModel ->
                         SingleLineViewInflater.inflatePublicSingleLineView(
@@ -494,13 +495,13 @@
                     }
             }
 
-            logger.logAsyncTaskProgress(entry, "loading RON images")
+            logger.logAsyncTaskProgress(entry.logKey, "loading RON images")
             inflationProgress.rowImageInflater.loadImagesSynchronously(packageContext)
 
-            logger.logAsyncTaskProgress(entry, "getting row image resolver (on wrong thread!)")
+            logger.logAsyncTaskProgress(entry.logKey, "getting row image resolver (on wrong thread!)")
             val imageResolver = row.imageResolver
             // wait for image resolver to finish preloading
-            logger.logAsyncTaskProgress(entry, "waiting for preloaded images")
+            logger.logAsyncTaskProgress(entry.logKey, "waiting for preloaded images")
             imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS)
             return inflationProgress
         }
@@ -547,13 +548,13 @@
         }
 
         override fun abort() {
-            logger.logAsyncTaskProgress(entry, "cancelling inflate")
+            logger.logAsyncTaskProgress(entry.logKey, "cancelling inflate")
             cancel(/* mayInterruptIfRunning= */ true)
             if (cancellationSignal != null) {
-                logger.logAsyncTaskProgress(entry, "cancelling apply")
+                logger.logAsyncTaskProgress(entry.logKey, "cancelling apply")
                 cancellationSignal!!.cancel()
             }
-            logger.logAsyncTaskProgress(entry, "aborted")
+            logger.logAsyncTaskProgress(entry.logKey, "aborted")
         }
 
         override fun handleInflationException(e: Exception) {
@@ -641,11 +642,11 @@
                 (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0 &&
                     result.remoteViews.headsUp != null)
             if (inflateContracted || inflateExpanded || inflateHeadsUp) {
-                logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state")
+                logger.logAsyncTaskProgress(entry.logKey, "inflating contracted smart reply state")
                 result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry)
             }
             if (inflateExpanded) {
-                logger.logAsyncTaskProgress(entry, "inflating expanded smart reply state")
+                logger.logAsyncTaskProgress(entry.logKey, "inflating expanded smart reply state")
                 result.expandedInflatedSmartReplies =
                     inflater.inflateSmartReplyViewHolder(
                         context,
@@ -656,7 +657,7 @@
                     )
             }
             if (inflateHeadsUp) {
-                logger.logAsyncTaskProgress(entry, "inflating heads up smart reply state")
+                logger.logAsyncTaskProgress(entry.logKey, "inflating heads up smart reply state")
                 result.headsUpInflatedSmartReplies =
                     inflater.inflateSmartReplyViewHolder(
                         context,
@@ -687,13 +688,16 @@
 
             val promotedContent =
                 if (PromotedNotificationContentModel.featureFlagEnabled()) {
-                    logger.logAsyncTaskProgress(entry, "extracting promoted notification content")
+                    logger.logAsyncTaskProgress(
+                        entry.logKey,
+                        "extracting promoted notification content"
+                    )
                     val imageModelProvider = rowImageInflater.useForContentModel()
                     promotedNotificationContentExtractor
                         .extractContent(entry, builder, imageModelProvider)
                         .also {
                             logger.logAsyncTaskProgress(
-                                entry,
+                                entry.logKey,
                                 "extracted promoted notification content: $it",
                             )
                         }
@@ -726,7 +730,7 @@
                     AsyncHybridViewInflation.isEnabled &&
                         reInflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
                 ) {
-                    logger.logAsyncTaskProgress(entry, "inflating single line view model")
+                    logger.logAsyncTaskProgress(entry.logKey, "inflating single line view model")
                     SingleLineViewInflater.inflateSingleLineViewModel(
                         notification = entry.sbn.notification,
                         messagingStyle = messagingStyle,
@@ -743,7 +747,10 @@
                     LockscreenOtpRedaction.isEnabled &&
                         reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE != 0
                 ) {
-                    logger.logAsyncTaskProgress(entry, "inflating public single line view model")
+                    logger.logAsyncTaskProgress(
+                        entry.logKey,
+                        "inflating public single line view model"
+                    )
                     if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
                         SingleLineViewInflater.inflateSingleLineViewModel(
                             notification = entry.sbn.notification,
@@ -834,11 +841,10 @@
             logger: NotificationRowContentBinderLogger,
         ): NewRemoteViews {
             return TraceUtils.trace("NotificationContentInflater.createRemoteViews") {
-                val entryForLogging: NotificationEntry = row.entry
                 val contracted =
                     if (reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0) {
                         logger.logAsyncTaskProgress(
-                            entryForLogging,
+                            row.loggingKey,
                             "creating contracted remote view",
                         )
                         createContentView(builder, bindParams.isMinimized)
@@ -846,7 +852,7 @@
                 val expanded =
                     if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) {
                         logger.logAsyncTaskProgress(
-                            entryForLogging,
+                            row.loggingKey,
                             "creating expanded remote view",
                         )
                         createExpandedView(builder, bindParams.isMinimized)
@@ -854,7 +860,7 @@
                 val headsUp =
                     if (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0) {
                         logger.logAsyncTaskProgress(
-                            entryForLogging,
+                            row.loggingKey,
                             "creating heads up remote view",
                         )
                         val isHeadsUpCompact = headsUpStyleProvider.shouldApplyCompactStyle()
@@ -866,7 +872,10 @@
                     } else null
                 val public =
                     if (reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC != 0) {
-                        logger.logAsyncTaskProgress(entryForLogging, "creating public remote view")
+                        logger.logAsyncTaskProgress(
+                            row.loggingKey,
+                            "creating public remote view"
+                        )
                         if (
                             LockscreenOtpRedaction.isEnabled &&
                                 bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT
@@ -888,7 +897,7 @@
                             reInflateFlags and FLAG_GROUP_SUMMARY_HEADER != 0
                     ) {
                         logger.logAsyncTaskProgress(
-                            entryForLogging,
+                            row.loggingKey,
                             "creating group summary remote view",
                         )
                         builder.makeNotificationGroupHeader()
@@ -899,7 +908,7 @@
                             reInflateFlags and FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER != 0
                     ) {
                         logger.logAsyncTaskProgress(
-                            entryForLogging,
+                            row.loggingKey,
                             "creating low-priority group summary remote view",
                         )
                         builder.makeLowPriorityContentView(true /* useRegularSubtext */)
@@ -971,14 +980,14 @@
                 val applyCallback: ApplyCallback =
                     object : ApplyCallback() {
                         override fun setResultView(v: View) {
-                            logger.logAsyncTaskProgress(entry, "contracted view applied")
+                            logger.logAsyncTaskProgress(entry.logKey, "contracted view applied")
                             result.inflatedContentView = v
                         }
 
                         override val remoteView: RemoteViews
                             get() = result.remoteViews.contracted
                     }
-                logger.logAsyncTaskProgress(entry, "applying contracted view")
+                logger.logAsyncTaskProgress(entry.logKey, "applying contracted view")
                 applyRemoteView(
                     inflationExecutor = inflationExecutor,
                     inflateSynchronously = inflateSynchronously,
@@ -1010,14 +1019,14 @@
                 val applyCallback: ApplyCallback =
                     object : ApplyCallback() {
                         override fun setResultView(v: View) {
-                            logger.logAsyncTaskProgress(entry, "expanded view applied")
+                            logger.logAsyncTaskProgress(entry.logKey, "expanded view applied")
                             result.inflatedExpandedView = v
                         }
 
                         override val remoteView: RemoteViews
                             get() = result.remoteViews.expanded
                     }
-                logger.logAsyncTaskProgress(entry, "applying expanded view")
+                logger.logAsyncTaskProgress(entry.logKey, "applying expanded view")
                 applyRemoteView(
                     inflationExecutor = inflationExecutor,
                     inflateSynchronously = inflateSynchronously,
@@ -1049,14 +1058,14 @@
                 val applyCallback: ApplyCallback =
                     object : ApplyCallback() {
                         override fun setResultView(v: View) {
-                            logger.logAsyncTaskProgress(entry, "heads up view applied")
+                            logger.logAsyncTaskProgress(entry.logKey, "heads up view applied")
                             result.inflatedHeadsUpView = v
                         }
 
                         override val remoteView: RemoteViews
                             get() = result.remoteViews.headsUp
                     }
-                logger.logAsyncTaskProgress(entry, "applying heads up view")
+                logger.logAsyncTaskProgress(entry.logKey, "applying heads up view")
                 applyRemoteView(
                     inflationExecutor = inflationExecutor,
                     inflateSynchronously = inflateSynchronously,
@@ -1088,14 +1097,14 @@
                 val applyCallback: ApplyCallback =
                     object : ApplyCallback() {
                         override fun setResultView(v: View) {
-                            logger.logAsyncTaskProgress(entry, "public view applied")
+                            logger.logAsyncTaskProgress(entry.logKey, "public view applied")
                             result.inflatedPublicView = v
                         }
 
                         override val remoteView: RemoteViews
                             get() = result.remoteViews.public!!
                     }
-                logger.logAsyncTaskProgress(entry, "applying public view")
+                logger.logAsyncTaskProgress(entry.logKey, "applying public view")
                 applyRemoteView(
                     inflationExecutor = inflationExecutor,
                     inflateSynchronously = inflateSynchronously,
@@ -1130,14 +1139,17 @@
                     val applyCallback: ApplyCallback =
                         object : ApplyCallback() {
                             override fun setResultView(v: View) {
-                                logger.logAsyncTaskProgress(entry, "group header view applied")
+                                logger.logAsyncTaskProgress(
+                                    entry.logKey,
+                                    "group header view applied"
+                                )
                                 result.inflatedGroupHeaderView = v as NotificationHeaderView?
                             }
 
                             override val remoteView: RemoteViews
                                 get() = result.remoteViews.normalGroupHeader!!
                         }
-                    logger.logAsyncTaskProgress(entry, "applying group header view")
+                    logger.logAsyncTaskProgress(entry.logKey, "applying group header view")
                     applyRemoteView(
                         inflationExecutor = inflationExecutor,
                         inflateSynchronously = inflateSynchronously,
@@ -1173,7 +1185,7 @@
                         object : ApplyCallback() {
                             override fun setResultView(v: View) {
                                 logger.logAsyncTaskProgress(
-                                    entry,
+                                    entry.logKey,
                                     "low-priority group header view applied",
                                 )
                                 result.inflatedMinimizedGroupHeaderView =
@@ -1183,7 +1195,10 @@
                             override val remoteView: RemoteViews
                                 get() = result.remoteViews.minimizedGroupHeader!!
                         }
-                    logger.logAsyncTaskProgress(entry, "applying low priority group header view")
+                    logger.logAsyncTaskProgress(
+                        entry.logKey,
+                        "applying low priority group header view"
+                    )
                     applyRemoteView(
                         inflationExecutor = inflationExecutor,
                         inflateSynchronously = inflateSynchronously,
@@ -1221,7 +1236,7 @@
             )
             val cancellationSignal = CancellationSignal()
             cancellationSignal.setOnCancelListener {
-                logger.logAsyncTaskProgress(entry, "apply cancelled")
+                logger.logAsyncTaskProgress(entry.logKey, "apply cancelled")
                 Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row))
                 runningInflations.values.forEach(
                     Consumer { obj: CancellationSignal -> obj.cancel() }
@@ -1278,7 +1293,7 @@
                     handleInflationError(
                         runningInflations,
                         e,
-                        row.entry,
+                        row,
                         callback,
                         logger,
                         "applying view synchronously",
@@ -1303,7 +1318,7 @@
                             handleInflationError(
                                 runningInflations,
                                 InflationException(invalidReason),
-                                row.entry,
+                                row,
                                 callback,
                                 logger,
                                 "applied invalid view",
@@ -1362,7 +1377,7 @@
                             handleInflationError(
                                 runningInflations,
                                 e,
-                                row.entry,
+                                row,
                                 callback,
                                 logger,
                                 "applying view",
@@ -1465,15 +1480,15 @@
         private fun handleInflationError(
             runningInflations: HashMap<Int, CancellationSignal>,
             e: Exception,
-            notification: NotificationEntry,
+            notification: ExpandableNotificationRow?,
             callback: InflationCallback?,
             logger: NotificationRowContentBinderLogger,
             logContext: String,
         ) {
             Assert.isMainThread()
-            logger.logAsyncTaskException(notification, logContext, e)
+            logger.logAsyncTaskException(notification?.loggingKey, logContext, e)
             runningInflations.values.forEach(Consumer { obj: CancellationSignal -> obj.cancel() })
-            callback?.handleInflationException(notification, e)
+            callback?.handleInflationException(notification?.entry, e)
         }
 
         /**
@@ -1496,7 +1511,7 @@
             if (runningInflations.isNotEmpty()) {
                 return false
             }
-            logger.logAsyncTaskProgress(entry, "finishing")
+            logger.logAsyncTaskProgress(row.loggingKey, "finishing")
 
             // Put the new image index on the row
             row.mImageModelIndex = result.rowImageInflater.getNewImageIndex()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
index a32e1d7..e94b2dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
@@ -35,66 +35,66 @@
 class NotificationRowContentBinderLogger
 @Inject
 constructor(@NotifInflationLog private val buffer: LogBuffer) {
-    fun logNotBindingRowWasRemoved(entry: NotificationEntry) {
+    fun logNotBindingRowWasRemoved(entry: String) {
         buffer.log(
             TAG,
             LogLevel.INFO,
-            { str1 = entry.logKey },
+            { str1 = entry },
             { "not inflating $str1: row was removed" }
         )
     }
 
-    fun logBinding(entry: NotificationEntry, @InflationFlag flag: Int) {
+    fun logBinding(entry: String, @InflationFlag flag: Int) {
         buffer.log(
             TAG,
             LogLevel.DEBUG,
             {
-                str1 = entry.logKey
+                str1 = entry
                 int1 = flag
             },
             { "binding views ${flagToString(int1)} for $str1" }
         )
     }
 
-    fun logCancelBindAbortedTask(entry: NotificationEntry) {
+    fun logCancelBindAbortedTask(entry: String) {
         buffer.log(
             TAG,
             LogLevel.INFO,
-            { str1 = entry.logKey },
+            { str1 = entry },
             { "aborted task to cancel binding $str1" }
         )
     }
 
-    fun logUnbinding(entry: NotificationEntry, @InflationFlag flag: Int) {
+    fun logUnbinding(entry: String, @InflationFlag flag: Int) {
         buffer.log(
             TAG,
             LogLevel.DEBUG,
             {
-                str1 = entry.logKey
+                str1 = entry
                 int1 = flag
             },
             { "unbinding views ${flagToString(int1)} for $str1" }
         )
     }
 
-    fun logAsyncTaskProgress(entry: NotificationEntry, progress: String) {
+    fun logAsyncTaskProgress(entry: String?, progress: String) {
         buffer.log(
             TAG,
             LogLevel.DEBUG,
             {
-                str1 = entry.logKey
+                str1 = entry
                 str2 = progress
             },
             { "async task for $str1: $str2" }
         )
     }
 
-    fun logAsyncTaskException(entry: NotificationEntry, logContext: String, exception: Throwable) {
+    fun logAsyncTaskException(entry: String?, logContext: String, exception: Throwable) {
         buffer.log(
             TAG,
             LogLevel.DEBUG,
             {
-                str1 = entry.logKey
+                str1 = entry
                 str2 = logContext
                 str3 = exception.stackTraceToString()
             },
@@ -103,7 +103,7 @@
     }
 
     fun logInflateSingleLine(
-        entry: NotificationEntry,
+        entry: String?,
         @InflationFlag inflationFlags: Int,
         isConversation: Boolean
     ) {
@@ -111,7 +111,7 @@
             TAG,
             LogLevel.DEBUG,
             {
-                str1 = entry.logKey
+                str1 = entry
                 int1 = inflationFlags
                 bool1 = isConversation
             },
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
index ea73b4b..e0c1692 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
@@ -417,8 +417,8 @@
     ): HybridNotificationView? {
         if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return null
 
-        logger.logInflateSingleLine(entry, reinflateFlags, isConversation)
-        logger.logAsyncTaskProgress(entry, "inflating single-line content view")
+        logger.logInflateSingleLine(entry.logKey, reinflateFlags, isConversation)
+        logger.logAsyncTaskProgress(entry.logKey, "inflating single-line content view")
 
         var view: HybridNotificationView? = null
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
index da98858..9bd5a5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
@@ -291,7 +291,7 @@
          * currently being swiped. From the center outwards, the multipliers apply to the neighbors
          * of the swiped view.
          */
-        private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.18f, 0.28f, 0.5f, 0.28f, 0.18f)
+        private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.04f, 0.12f, 0.5f, 0.12f, 0.04f)
 
         const val MAGNETIC_REDUCTION = 0.65f
 
@@ -299,7 +299,7 @@
         private const val DETACH_STIFFNESS = 800f
         private const val DETACH_DAMPING_RATIO = 0.95f
         private const val SNAP_BACK_STIFFNESS = 550f
-        private const val SNAP_BACK_DAMPING_RATIO = 0.52f
+        private const val SNAP_BACK_DAMPING_RATIO = 0.6f
 
         // Maximum value of corner roundness that gets applied during the pre-detach dragging
         private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 1d18535..048958e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -1458,7 +1458,7 @@
                 if (AsyncHybridViewInflation.isEnabled()) {
                     minExpandHeight += mMinSingleLineHeight;
                 } else {
-                    Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey()
+                    Log.e(TAG, "getMinHeight: child " + child.getKey()
                             + " single line view is null", new Exception());
                 }
             }
@@ -1688,8 +1688,8 @@
     public void addTransientView(View view, int index) {
         if (mLogger != null && view instanceof ExpandableNotificationRow) {
             mLogger.addTransientRow(
-                    ((ExpandableNotificationRow) view).getEntry(),
-                    getContainingNotification().getEntry(),
+                    ((ExpandableNotificationRow) view).getLoggingKey(),
+                    getContainingNotification().getLoggingKey(),
                     index
             );
         }
@@ -1700,8 +1700,8 @@
     public void removeTransientView(View view) {
         if (mLogger != null && view instanceof ExpandableNotificationRow) {
             mLogger.removeTransientRow(
-                    ((ExpandableNotificationRow) view).getEntry(),
-                    getContainingNotification().getEntry()
+                    ((ExpandableNotificationRow) view).getLoggingKey(),
+                    getContainingNotification().getLoggingKey()
             );
         }
         super.removeTransientView(view);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt
index 4986b63..d8da412 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt
@@ -27,16 +27,16 @@
 @Inject
 constructor(@NotificationRenderLog private val notificationRenderBuffer: LogBuffer) {
     fun addTransientRow(
-        childEntry: NotificationEntry,
-        containerEntry: NotificationEntry,
+        childEntry: String,
+        containerEntry: String,
         index: Int
     ) {
         notificationRenderBuffer.log(
             TAG,
             LogLevel.INFO,
             {
-                str1 = childEntry.logKey
-                str2 = containerEntry.logKey
+                str1 = childEntry
+                str2 = containerEntry
                 int1 = index
             },
             { "addTransientRow: childKey: $str1 -- containerKey: $str2 -- index: $int1" }
@@ -44,15 +44,15 @@
     }
 
     fun removeTransientRow(
-        childEntry: NotificationEntry,
-        containerEntry: NotificationEntry,
+        childEntry: String,
+        containerEntry: String,
     ) {
         notificationRenderBuffer.log(
             TAG,
             LogLevel.INFO,
             {
-                str1 = childEntry.logKey
-                str2 = containerEntry.logKey
+                str1 = childEntry
+                str2 = containerEntry
             },
             { "removeTransientRow: childKey: $str1 -- containerKey: $str2" }
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index 043d64e..3d8fe01 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -278,13 +278,13 @@
             val fs =
                 when (val first = s.firstVisibleChild) {
                     null -> "(null)"
-                    is ExpandableNotificationRow -> first.entry.key
+                    is ExpandableNotificationRow -> first.loggingKey
                     else -> Integer.toHexString(System.identityHashCode(first))
                 }
             val ls =
                 when (val last = s.lastVisibleChild) {
                     null -> "(null)"
-                    is ExpandableNotificationRow -> last.entry.key
+                    is ExpandableNotificationRow -> last.loggingKey
                     else -> Integer.toHexString(System.identityHashCode(last))
                 }
             Log.d(TAG, "updateSections: f=$fs s=$i")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index fdb0e73..6313258 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -789,17 +789,17 @@
     private void logHunSkippedForUnexpectedState(ExpandableNotificationRow enr,
                                                  boolean expected, boolean actual) {
         if (mLogger == null) return;
-        mLogger.hunSkippedForUnexpectedState(enr.getEntry(), expected, actual);
+        mLogger.hunSkippedForUnexpectedState(enr.getLoggingKey(), expected, actual);
     }
 
     private void logHunAnimationSkipped(ExpandableNotificationRow enr, String reason) {
         if (mLogger == null) return;
-        mLogger.hunAnimationSkipped(enr.getEntry(), reason);
+        mLogger.hunAnimationSkipped(enr.getLoggingKey(), reason);
     }
 
     private void logHunAnimationEventAdded(ExpandableNotificationRow enr, int type) {
         if (mLogger == null) return;
-        mLogger.hunAnimationEventAdded(enr.getEntry(), type);
+        mLogger.hunAnimationEventAdded(enr.getLoggingKey(), type);
     }
 
     private void onDrawDebug(Canvas canvas) {
@@ -1810,16 +1810,22 @@
 
     private ExpandableNotificationRow getTopHeadsUpRow() {
         ExpandableNotificationRow row = mTopHeadsUpRow;
-        if (row.isChildInGroup()) {
-            final NotificationEntry groupSummary =
-                    mGroupMembershipManager.getGroupSummary(row.getEntry());
-            if (groupSummary != null) {
-                row = groupSummary.getRow();
+        if (NotificationBundleUi.isEnabled()) {
+            if (mGroupMembershipManager.isChildInGroup(row.getEntryAdapter())
+                    && row.isChildInGroup()) {
+                row = row.getNotificationParent();
+            }
+        } else {
+            if (row.isChildInGroup()) {
+                final NotificationEntry groupSummary =
+                        mGroupMembershipManager.getGroupSummary(row.getEntry());
+                if (groupSummary != null) {
+                    row = groupSummary.getRow();
+                }
             }
         }
         return row;
     }
-
     /**
      * @return the position from where the appear transition ends when expanding.
      * Measured in absolute height.
@@ -1966,10 +1972,19 @@
                     && touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
                 if (slidingChild instanceof ExpandableNotificationRow row) {
                     NotificationEntry entry = row.getEntry();
+                    boolean isEntrySummaryForTopHun;
+                    if (NotificationBundleUi.isEnabled()) {
+                        isEntrySummaryForTopHun = Objects.equals(
+                                ((ExpandableNotificationRow) slidingChild).getNotificationParent(),
+                                mTopHeadsUpRow);
+                    } else {
+                        isEntrySummaryForTopHun = mTopHeadsUpRow != null &&
+                                mGroupMembershipManager.getGroupSummary(mTopHeadsUpRow.getEntry())
+                                == entry;
+                    }
                     if (!mIsExpanded && row.isHeadsUp() && row.isPinned()
                             && mTopHeadsUpRow != row
-                            && mGroupMembershipManager.getGroupSummary(mTopHeadsUpRow.getEntry())
-                            != entry) {
+                            && !isEntrySummaryForTopHun) {
                         continue;
                     }
                     return row.getViewAtPosition(touchY - childTop);
@@ -2902,17 +2917,17 @@
         if (child instanceof ExpandableNotificationRow) {
             if (container instanceof NotificationChildrenContainer) {
                 mLogger.addTransientChildNotificationToChildContainer(
-                        ((ExpandableNotificationRow) child).getEntry(),
+                        ((ExpandableNotificationRow) child).getLoggingKey(),
                         ((NotificationChildrenContainer) container)
-                                .getContainingNotification().getEntry()
+                                .getContainingNotification().getLoggingKey()
                 );
             } else if (container instanceof NotificationStackScrollLayout) {
                 mLogger.addTransientChildNotificationToNssl(
-                        ((ExpandableNotificationRow) child).getEntry()
+                        ((ExpandableNotificationRow) child).getLoggingKey()
                 );
             } else {
                 mLogger.addTransientChildNotificationToViewGroup(
-                        ((ExpandableNotificationRow) child).getEntry(),
+                        ((ExpandableNotificationRow) child).getLoggingKey(),
                         container
                 );
             }
@@ -2922,7 +2937,7 @@
     @Override
     public void addTransientView(View view, int index) {
         if (mLogger != null && view instanceof ExpandableNotificationRow) {
-            mLogger.addTransientRow(((ExpandableNotificationRow) view).getEntry(), index);
+            mLogger.addTransientRow(((ExpandableNotificationRow) view).getLoggingKey(), index);
         }
         super.addTransientView(view, index);
     }
@@ -2930,7 +2945,7 @@
     @Override
     public void removeTransientView(View view) {
         if (mLogger != null && view instanceof ExpandableNotificationRow) {
-            mLogger.removeTransientRow(((ExpandableNotificationRow) view).getEntry());
+            mLogger.removeTransientRow(((ExpandableNotificationRow) view).getLoggingKey());
         }
         super.removeTransientView(view);
     }
@@ -2981,7 +2996,7 @@
         String key = "";
         if (mDebugRemoveAnimation) {
             if (child instanceof ExpandableNotificationRow) {
-                key = ((ExpandableNotificationRow) child).getEntry().getKey();
+                key = ((ExpandableNotificationRow) child).getKey();
             }
             Log.d(TAG, "generateRemoveAnimation " + key);
         }
@@ -3403,7 +3418,7 @@
                         + " isHeadsUp=" + isHeadsUp
                         + " type=" + type
                         + " onBottom=" + onBottom
-                        + " row=" + row.getEntry().getKey());
+                        + " row=" + row.getKey());
             }
             logHunAnimationEventAdded(row, type);
         }
@@ -3478,7 +3493,7 @@
             if (mDebugRemoveAnimation) {
                 String key = "";
                 if (child instanceof ExpandableNotificationRow) {
-                    key = ((ExpandableNotificationRow) child).getEntry().getKey();
+                    key = ((ExpandableNotificationRow) child).getKey();
                 }
                 Log.d(TAG, "created Remove Event - SwipedOut: " + childWasSwipedOut + " " + key);
             }
@@ -4366,7 +4381,7 @@
         if (mLogger == null) {
             return;
         }
-        mLogger.transientNotificationRowTraversalCleaned(transientView.getEntry(), reason);
+        mLogger.transientNotificationRowTraversalCleaned(transientView.getLoggingKey(), reason);
     }
 
     void onPanelTrackingStarted() {
@@ -5060,7 +5075,7 @@
                     + " addAnimation=" + addAnimation
                     + (row.getEntry() == null ? " entry NULL "
                             : " isSeenInShade=" + row.getEntry().isSeenInShade()
-                                    + " row=" + row.getEntry().getKey())
+                                    + " row=" + row.getKey())
                     + " mIsExpanded=" + mIsExpanded
                     + " isHeadsUp=" + isHeadsUp);
         }
@@ -6766,7 +6781,7 @@
         NotificationChildrenContainer childrenContainer = row.getChildrenContainer();
         if (childrenContainer == null) {
             Log.wtf(TAG, "Tried to update group header alignment for something that's "
-                    + "not a group; key = " + row.getEntry().getKey());
+                    + "not a group; key = " + row.getKey());
             return;
         }
         NotificationHeaderView header = childrenContainer.getGroupHeader();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 01ef90a..5c96470 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -553,7 +553,7 @@
                     if (view instanceof ExpandableNotificationRow row) {
                         if (row.isHeadsUp()) {
                             mHeadsUpManager.addSwipedOutNotification(
-                                    row.getEntry().getSbn().getKey());
+                                    row.getKey());
                         }
                         row.performDismiss(false /* fromAccessibility */);
                     }
@@ -598,7 +598,7 @@
                                 && (parent.areGutsExposed()
                                 || mSwipeHelper.getExposedMenuView() == parent
                                 || (parent.getAttachedChildren().size() == 1
-                                && mDismissibilityProvider.isDismissable(parent.getEntry())))) {
+                                && mDismissibilityProvider.isDismissable(parent.getKey())))) {
                             // In this case the group is expanded and showing the menu for the
                             // group, further interaction should apply to the group, not any
                             // child notifications so we use the parent of the child. We also do the
@@ -642,7 +642,7 @@
                                 && row.getEntry().getSbn().getNotification().fullScreenIntent
                                 == null) {
                             mHeadsUpManager.removeNotification(
-                                    row.getEntry().getSbn().getKey(),
+                                    row.getKey(),
                                     /* removeImmediately= */ true,
                                     /* reason= */ "onChildSnappedBack"
                             );
@@ -1955,12 +1955,11 @@
         @Override
         public void bindRow(ExpandableNotificationRow row) {
             row.setHeadsUpAnimatingAwayListener(animatingAway -> {
-                NotificationEntry entry = row.getEntry();
-                mHeadsUpAppearanceController.updateHeader(entry);
-                mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(entry);
+                mHeadsUpAppearanceController.updateHeader(row);
+                mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(row);
                 if (GroupHunAnimationFix.isEnabled() && !animatingAway) {
                     // invalidate list to make sure the row is sorted to the correct section
-                    mHeadsUpManager.onEntryAnimatingAwayEnded(entry);
+                    mHeadsUpManager.onEntryAnimatingAwayEnded(row.getEntry());
                 }
             });
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 30658710..871b81d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -23,15 +23,15 @@
     @NotificationRenderLog private val notificationRenderBuffer: LogBuffer,
     @ShadeLog private val shadeLogBuffer: LogBuffer,
 ) {
-    fun hunAnimationSkipped(entry: NotificationEntry, reason: String) {
+    fun hunAnimationSkipped(entry: String, reason: String) {
         buffer.log(TAG, INFO, {
-            str1 = entry.logKey
+            str1 = entry
             str2 = reason
         }, {
             "heads up animation skipped: key: $str1 reason: $str2"
         })
     }
-    fun hunAnimationEventAdded(entry: NotificationEntry, type: Int) {
+    fun hunAnimationEventAdded(entry: String, type: Int) {
         val reason: String
         reason = if (type == ANIMATION_TYPE_HEADS_UP_DISAPPEAR) {
             "HEADS_UP_DISAPPEAR"
@@ -47,16 +47,16 @@
             type.toString()
         }
         buffer.log(TAG, INFO, {
-            str1 = entry.logKey
+            str1 = entry
             str2 = reason
         }, {
             "heads up animation added: $str1 with type $str2"
         })
     }
 
-    fun hunSkippedForUnexpectedState(entry: NotificationEntry, expected: Boolean, actual: Boolean) {
+    fun hunSkippedForUnexpectedState(entry: String, expected: Boolean, actual: Boolean) {
         buffer.log(TAG, INFO, {
-            str1 = entry.logKey
+            str1 = entry
             bool1 = expected
             bool2 = actual
         }, {
@@ -84,9 +84,9 @@
         })
     }
 
-    fun transientNotificationRowTraversalCleaned(entry: NotificationEntry, reason: String) {
+    fun transientNotificationRowTraversalCleaned(entry: String, reason: String) {
         notificationRenderBuffer.log(TAG, INFO, {
-            str1 = entry.logKey
+            str1 = entry
             str2 = reason
         }, {
             "transientNotificationRowTraversalCleaned: key: $str1 reason: $str2"
@@ -94,12 +94,12 @@
     }
 
     fun addTransientChildNotificationToChildContainer(
-            childEntry: NotificationEntry,
-            containerEntry: NotificationEntry,
+            childEntry: String,
+            containerEntry: String,
     ) {
         notificationRenderBuffer.log(TAG, INFO, {
-            str1 = childEntry.logKey
-            str2 = containerEntry.logKey
+            str1 = childEntry
+            str2 = containerEntry
         }, {
             "addTransientChildToContainer from onViewRemovedInternal: childKey: $str1 " +
                     "-- containerKey: $str2"
@@ -107,21 +107,21 @@
     }
 
     fun addTransientChildNotificationToNssl(
-            childEntry: NotificationEntry,
+            childEntry: String,
     ) {
         notificationRenderBuffer.log(TAG, INFO, {
-            str1 = childEntry.logKey
+            str1 = childEntry
         }, {
             "addTransientRowToNssl from onViewRemovedInternal: childKey: $str1"
         })
     }
 
     fun addTransientChildNotificationToViewGroup(
-            childEntry: NotificationEntry,
+            childEntry: String,
             container: ViewGroup
     ) {
         notificationRenderBuffer.log(TAG, ERROR, {
-            str1 = childEntry.logKey
+            str1 = childEntry
             str2 = container.toString()
         }, {
             "addTransientRowTo unhandled ViewGroup from onViewRemovedInternal: childKey: $str1 " +
@@ -130,14 +130,14 @@
     }
 
     fun addTransientRow(
-            childEntry: NotificationEntry,
+            childEntry: String,
             index: Int
     ) {
         notificationRenderBuffer.log(
                 TAG,
                 INFO,
                 {
-                    str1 = childEntry.logKey
+                    str1 = childEntry
                     int1 = index
                 },
                 { "addTransientRow to NSSL: childKey: $str1 -- index: $int1" }
@@ -145,13 +145,13 @@
     }
 
     fun removeTransientRow(
-            childEntry: NotificationEntry,
+            childEntry: String,
     ) {
         notificationRenderBuffer.log(
                 TAG,
                 INFO,
                 {
-                    str1 = childEntry.logKey
+                    str1 = childEntry
                 },
                 { "removeTransientRow from NSSL: childKey: $str1" }
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 08692be..88d3ad8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -275,11 +275,7 @@
     public static void debugLogView(View view, String s) {
         String viewString = "";
         if (view instanceof ExpandableNotificationRow row) {
-            if (row.getEntry() == null) {
-                viewString = "ExpandableNotificationRow has null NotificationEntry";
-            } else {
-                viewString = row.getEntry().getSbn().getId() + "";
-            }
+            viewString = row.getKey();
         } else if (view == null) {
             viewString = "View is null";
         } else if (view instanceof SectionHeaderView) {
@@ -413,10 +409,8 @@
      */
     public boolean isCyclingOut(ExpandableNotificationRow row, AmbientState ambientState) {
         if (!NotificationHeadsUpCycling.isEnabled()) return false;
-        if (row.getEntry() == null) return false;
-        if (row.getEntry().getKey() == null) return false;
         String cyclingOutKey = ambientState.getAvalanchePreviousHunKey();
-        return row.getEntry().getKey().equals(cyclingOutKey);
+        return row.getKey().equals(cyclingOutKey);
     }
 
     /**
@@ -424,10 +418,8 @@
      */
     public boolean isCyclingIn(ExpandableNotificationRow row, AmbientState ambientState) {
         if (!NotificationHeadsUpCycling.isEnabled()) return false;
-        if (row.getEntry() == null) return false;
-        if (row.getEntry().getKey() == null) return false;
         String cyclingInKey = ambientState.getAvalancheShowingHunKey();
-        return row.getEntry().getKey().equals(cyclingInKey);
+        return row.getKey().equals(cyclingInKey);
     }
 
     /** Updates the dimmed and hiding sensitive states of the children. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index 2b05223..4da418e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -422,7 +422,7 @@
             if (changingView instanceof ExpandableNotificationRow && mLogger != null) {
                 loggable = true;
                 isHeadsUp = ((ExpandableNotificationRow) changingView).isHeadsUp();
-                key = ((ExpandableNotificationRow) changingView).getEntry().getKey();
+                key = ((ExpandableNotificationRow) changingView).getKey();
             }
             if (event.animationType ==
                     NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 548ab83..7f4ac37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -215,13 +215,13 @@
     @Override
     public void onHeadsUpPinned(NotificationEntry entry) {
         updatePinnedStatus();
-        updateHeader(entry);
-        updateHeadsUpAndPulsingRoundness(entry);
+        updateHeader(entry.getRow());
+        updateHeadsUpAndPulsingRoundness(entry.getRow());
     }
 
     @Override
     public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) {
-        updateHeadsUpAndPulsingRoundness(entry);
+        updateHeadsUpAndPulsingRoundness(entry.getRow());
         mPhoneStatusBarTransitions.onHeadsUpStateChanged(isHeadsUp);
     }
 
@@ -416,8 +416,8 @@
     @Override
     public void onHeadsUpUnPinned(NotificationEntry entry) {
         updatePinnedStatus();
-        updateHeader(entry);
-        updateHeadsUpAndPulsingRoundness(entry);
+        updateHeader(entry.getRow());
+        updateHeadsUpAndPulsingRoundness(entry.getRow());
     }
 
     public void setAppearFraction(float expandedHeight, float appearFraction) {
@@ -448,9 +448,8 @@
         ExpandableNotificationRow previousTracked = mTrackedChild;
         mTrackedChild = trackedChild;
         if (previousTracked != null) {
-            NotificationEntry entry = previousTracked.getEntry();
-            updateHeader(entry);
-            updateHeadsUpAndPulsingRoundness(entry);
+            updateHeader(previousTracked);
+            updateHeadsUpAndPulsingRoundness(previousTracked);
         }
     }
 
@@ -460,13 +459,12 @@
 
     private void updateHeadsUpHeaders() {
         mHeadsUpManager.getAllEntries().forEach(entry -> {
-            updateHeader(entry);
-            updateHeadsUpAndPulsingRoundness(entry);
+            updateHeader(entry.getRow());
+            updateHeadsUpAndPulsingRoundness(entry.getRow());
         });
     }
 
-    public void updateHeader(NotificationEntry entry) {
-        ExpandableNotificationRow row = entry.getRow();
+    public void updateHeader(ExpandableNotificationRow row) {
         float headerVisibleAmount = 1.0f;
         // To fix the invisible HUN group header issue
         if (!AsyncGroupHeaderViewInflation.isEnabled()) {
@@ -480,10 +478,9 @@
 
     /**
      * Update the HeadsUp and the Pulsing roundness based on current state
-     * @param entry target notification
+     * @param row target notification row
      */
-    public void updateHeadsUpAndPulsingRoundness(NotificationEntry entry) {
-        ExpandableNotificationRow row = entry.getRow();
+    public void updateHeadsUpAndPulsingRoundness(ExpandableNotificationRow row) {
         boolean isTrackedChild = row == mTrackedChild;
         if (row.isPinned() || row.isHeadsUpAnimatingAway() || isTrackedChild) {
             float roundness = MathUtils.saturate(1f - mAppearFraction);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
index 686efb7..edaf400 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
@@ -34,9 +34,8 @@
     private val displayMetrics: DisplayMetrics
 ) {
     fun logUnSuccessfulDragDown(startingChild: View?) {
-        val entry = (startingChild as? ExpandableNotificationRow)?.entry
         buffer.log(TAG, LogLevel.INFO, {
-            str1 = entry?.key ?: "no entry"
+            str1 = (startingChild as? ExpandableNotificationRow)?.loggingKey ?: "no entry"
         }, {
             "Tried to drag down but can't drag down on $str1"
         })
@@ -49,27 +48,24 @@
     }
 
     fun logDragDownStarted(startingChild: ExpandableView?) {
-        val entry = (startingChild as? ExpandableNotificationRow)?.entry
         buffer.log(TAG, LogLevel.INFO, {
-            str1 = entry?.key ?: "no entry"
+            str1 = (startingChild as? ExpandableNotificationRow)?.loggingKey ?: "no entry"
         }, {
             "The drag down has started on $str1"
         })
     }
 
     fun logDraggedDownLockDownShade(startingChild: View?) {
-        val entry = (startingChild as? ExpandableNotificationRow)?.entry
         buffer.log(TAG, LogLevel.INFO, {
-            str1 = entry?.key ?: "no entry"
+            str1 = (startingChild as? ExpandableNotificationRow)?.loggingKey ?: "no entry"
         }, {
             "Dragged down in locked down shade on $str1"
         })
     }
 
     fun logDraggedDown(startingChild: View?, dragLengthY: Int) {
-        val entry = (startingChild as? ExpandableNotificationRow)?.entry
         buffer.log(TAG, LogLevel.INFO, {
-            str1 = entry?.key ?: "no entry"
+            str1 = (startingChild as? ExpandableNotificationRow)?.loggingKey ?: "no entry"
         }, {
             "Drag down succeeded on $str1"
         })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index ba41fd4..df1680a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -670,7 +670,7 @@
     }
 
     private void removeHunAfterClick(ExpandableNotificationRow row) {
-        String key = row.getEntry().getSbn().getKey();
+        String key = row.getKey();
         if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUpEntry(key)) {
             // Release the HUN notification to the shade.
             if (mPresenter.isPresenterFullyCollapsed()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 4d1d64e..74b1c3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -58,6 +58,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.AboveShelfObserver;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationAlertsInteractor;
@@ -262,6 +263,23 @@
         }
     }
 
+    @Override
+    public void onExpandClicked(ExpandableNotificationRow row, EntryAdapter clickedEntry,
+            boolean nowExpanded) {
+        mHeadsUpManager.setExpanded(clickedEntry.getKey(), row, nowExpanded);
+        mPowerInteractor.wakeUpIfDozing("NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE);
+        if (nowExpanded) {
+            if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+                mShadeTransitionController.goToLockedShade(row, /* needsQSAnimation = */ true);
+            } else if (clickedEntry.isSensitive().getValue() && isInLockedDownShade()) {
+                mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
+                // launch the bouncer if the device is locked
+                mActivityStarter.dismissKeyguardThenExecute(() -> false /* dismissAction */
+                        , null /* cancelRunnable */, false /* afterKeyguardGone */);
+            }
+        }
+    }
+
     /** @return true if the Shade is shown over the Lockscreen, and the device is locked */
     private boolean isInLockedDownShade() {
         if (SceneContainerFlag.isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index a0e3fbd..8b0f7c4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -29,18 +29,13 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.SliderDefaults
-import androidx.compose.material3.SliderState
-import androidx.compose.material3.VerticalSlider
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.pointerInput
@@ -49,16 +44,17 @@
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.theme.PlatformTheme
 import com.android.compose.ui.graphics.painter.DrawablePainter
+import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
 import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
-import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.res.R
 import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
 import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack
 import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
 import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
-import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
+import com.android.systemui.volume.ui.slider.AccessibilityParams
+import com.android.systemui.volume.ui.slider.Haptics
+import com.android.systemui.volume.ui.slider.Slider
 import javax.inject.Inject
-import kotlin.math.round
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.isActive
@@ -90,7 +86,7 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun VolumeDialogSlider(
     viewModel: VolumeDialogSliderViewModel,
@@ -108,59 +104,8 @@
         )
     val collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null)
     val sliderStateModel = collectedSliderStateModel ?: return
-
-    val steps = with(sliderStateModel.valueRange) { endInclusive - start - 1 }.toInt()
-
     val interactionSource = remember { MutableInteractionSource() }
-    val hapticsViewModel: SliderHapticsViewModel? =
-        hapticsViewModelFactory?.let {
-            rememberViewModel(traceName = "SliderHapticsViewModel") {
-                it.create(
-                    interactionSource,
-                    sliderStateModel.valueRange,
-                    Orientation.Vertical,
-                    VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
-                        sliderStateModel.valueRange
-                    ),
-                    VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
-                )
-            }
-        }
 
-    val sliderState =
-        remember(steps, sliderStateModel.valueRange) {
-            SliderState(
-                    value = sliderStateModel.value,
-                    valueRange = sliderStateModel.valueRange,
-                    steps = steps,
-                )
-                .also { sliderState ->
-                    sliderState.onValueChangeFinished = {
-                        viewModel.onSliderChangeFinished(sliderState.value)
-                        hapticsViewModel?.onValueChangeEnded()
-                    }
-                    sliderState.onValueChange = { newValue ->
-                        sliderState.value = newValue
-                        hapticsViewModel?.addVelocityDataPoint(newValue)
-                        overscrollViewModel.setSlider(
-                            value = sliderState.value,
-                            min = sliderState.valueRange.start,
-                            max = sliderState.valueRange.endInclusive,
-                        )
-                        viewModel.setStreamVolume(newValue, true)
-                    }
-                }
-        }
-
-    var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderStateModel.value)) }
-    LaunchedEffect(sliderStateModel.value) {
-        val value = sliderStateModel.value
-        sliderState.value = value
-        if (value != lastDiscreteStep) {
-            lastDiscreteStep = value
-            hapticsViewModel?.onValueChange(value)
-        }
-    }
     LaunchedEffect(interactionSource) {
         interactionSource.interactions.collect {
             when (it) {
@@ -171,24 +116,33 @@
         }
     }
 
-    VerticalSlider(
-        state = sliderState,
-        enabled = !sliderStateModel.isDisabled,
-        reverseDirection = true,
+    Slider(
+        value = sliderStateModel.value,
+        valueRange = sliderStateModel.valueRange,
+        onValueChanged = { value ->
+            overscrollViewModel.setSlider(
+                value = value,
+                min = sliderStateModel.valueRange.start,
+                max = sliderStateModel.valueRange.endInclusive,
+            )
+            viewModel.setStreamVolume(value, true)
+        },
+        onValueChangeFinished = { viewModel.onSliderChangeFinished(it) },
+        isEnabled = !sliderStateModel.isDisabled,
+        isReverseDirection = true,
+        isVertical = true,
         colors = colors,
         interactionSource = interactionSource,
-        modifier =
-            modifier.pointerInput(Unit) {
-                coroutineScope {
-                    val currentContext = currentCoroutineContext()
-                    awaitPointerEventScope {
-                        while (currentContext.isActive) {
-                            viewModel.onTouchEvent(awaitPointerEvent())
-                        }
-                    }
-                }
-            },
-        track = {
+        haptics =
+            hapticsViewModelFactory?.let {
+                Haptics.Enabled(
+                    hapticsViewModelFactory = it,
+                    hapticFilter = SliderHapticFeedbackFilter(),
+                    orientation = Orientation.Vertical,
+                )
+            } ?: Haptics.Disabled,
+        stepDistance = 1f,
+        track = { sliderState ->
             VolumeDialogSliderTrack(
                 sliderState,
                 colors = colors,
@@ -201,6 +155,19 @@
                 },
             )
         },
+        accessibilityParams =
+            AccessibilityParams(label = "", currentStateDescription = "", disabledMessage = ""),
+        modifier =
+            modifier.pointerInput(Unit) {
+                coroutineScope {
+                    val currentContext = currentCoroutineContext()
+                    awaitPointerEventScope {
+                        while (currentContext.isActive) {
+                            viewModel.onTouchEvent(awaitPointerEvent())
+                        }
+                    }
+                }
+            },
     )
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
index 3efb2b4..3d98eba 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
@@ -116,8 +116,8 @@
         override val isEnabled: Boolean
             get() = true
 
-        override val a11yStep: Int
-            get() = 1
+        override val a11yStep: Float
+            get() = 1f
 
         override val disabledMessage: String?
             get() = null
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index f9d776b..9d32285 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -165,7 +165,7 @@
             label = label,
             disabledMessage = disabledMessage,
             isEnabled = isEnabled,
-            a11yStep = volumeRange.step,
+            a11yStep = volumeRange.step.toFloat(),
             a11yClickDescription =
                 if (isAffectedByMute) {
                     context.getString(
@@ -307,7 +307,7 @@
         override val label: String,
         override val disabledMessage: String?,
         override val isEnabled: Boolean,
-        override val a11yStep: Int,
+        override val a11yStep: Float,
         override val a11yClickDescription: String?,
         override val a11yStateDescription: String?,
         override val isMutable: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index d74a433..a6c8091 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -86,7 +86,7 @@
             icon = Icon.Resource(R.drawable.ic_cast, null),
             label = context.getString(R.string.media_device_cast),
             isEnabled = true,
-            a11yStep = 1,
+            a11yStep = 1f,
         )
     }
 
@@ -96,7 +96,7 @@
         override val icon: Icon,
         override val label: String,
         override val isEnabled: Boolean,
-        override val a11yStep: Int,
+        override val a11yStep: Float,
     ) : SliderState {
         override val hapticFilter: SliderHapticFeedbackFilter
             get() = SliderHapticFeedbackFilter()
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index f135371..4bc237b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -36,7 +36,7 @@
      * A11y slider controls works by adjusting one step up or down. The default slider step isn't
      * enough to trigger rounding to the correct value.
      */
-    val a11yStep: Int
+    val a11yStep: Float
     val a11yClickDescription: String?
     val a11yStateDescription: String?
     val disabledMessage: String?
@@ -49,7 +49,7 @@
         override val icon: Icon? = null
         override val label: String = ""
         override val disabledMessage: String? = null
-        override val a11yStep: Int = 0
+        override val a11yStep: Float = 0f
         override val a11yClickDescription: String? = null
         override val a11yStateDescription: String? = null
         override val isEnabled: Boolean = true
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
new file mode 100644
index 0000000..d3562e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
@@ -0,0 +1,265 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+
+package com.android.systemui.volume.ui.slider
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderColors
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SliderState
+import androidx.compose.material3.VerticalSlider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.semantics.stateDescription
+import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
+import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.res.R
+import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
+import kotlin.math.round
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+private val defaultSpring =
+    SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh)
+private val defaultTrack: @Composable (SliderState) -> Unit =
+    @Composable { SliderDefaults.Track(it) }
+
+@Composable
+fun Slider(
+    value: Float,
+    valueRange: ClosedFloatingPointRange<Float>,
+    onValueChanged: (Float) -> Unit,
+    onValueChangeFinished: ((Float) -> Unit)?,
+    stepDistance: Float,
+    isEnabled: Boolean,
+    accessibilityParams: AccessibilityParams,
+    modifier: Modifier = Modifier,
+    colors: SliderColors = SliderDefaults.colors(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    haptics: Haptics = Haptics.Disabled,
+    isVertical: Boolean = false,
+    isReverseDirection: Boolean = false,
+    track: (@Composable (SliderState) -> Unit)? = null,
+) {
+    require(stepDistance > 0) { "stepDistance must be positive" }
+    val coroutineScope = rememberCoroutineScope()
+    val snappedValue = snapValue(value, valueRange, stepDistance)
+    val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource)
+
+    val animatable = remember { Animatable(snappedValue) }
+    var animationJob: Job? by remember { mutableStateOf(null) }
+    val sliderState =
+        remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) }
+    val valueChange: (Float) -> Unit = { newValue ->
+        hapticsViewModel?.onValueChange(newValue)
+        val snappedNewValue = snapValue(newValue, valueRange, stepDistance)
+        if (animatable.targetValue != snappedNewValue) {
+            onValueChanged(snappedNewValue)
+            animationJob?.cancel()
+            animationJob =
+                coroutineScope.launch {
+                    animatable.animateTo(
+                        targetValue = snappedNewValue,
+                        animationSpec = defaultSpring,
+                    )
+                }
+        }
+    }
+    val semantics =
+        accessibilityParams.createSemantics(
+            animatable.targetValue,
+            valueRange,
+            valueChange,
+            isEnabled,
+            stepDistance,
+        )
+
+    LaunchedEffect(snappedValue) {
+        if (!animatable.isRunning && animatable.targetValue != snappedValue) {
+            animationJob?.cancel()
+            animationJob =
+                coroutineScope.launch {
+                    animatable.animateTo(targetValue = snappedValue, animationSpec = defaultSpring)
+                }
+        }
+    }
+
+    sliderState.onValueChangeFinished = {
+        hapticsViewModel?.onValueChangeEnded()
+        onValueChangeFinished?.invoke(animatable.targetValue)
+    }
+    sliderState.onValueChange = valueChange
+    sliderState.value = animatable.value
+
+    if (isVertical) {
+        VerticalSlider(
+            state = sliderState,
+            enabled = isEnabled,
+            reverseDirection = isReverseDirection,
+            interactionSource = interactionSource,
+            colors = colors,
+            track = track ?: defaultTrack,
+            modifier = modifier.clearAndSetSemantics(semantics),
+        )
+    } else {
+        Slider(
+            state = sliderState,
+            enabled = isEnabled,
+            interactionSource = interactionSource,
+            colors = colors,
+            track = track ?: defaultTrack,
+            modifier = modifier.clearAndSetSemantics(semantics),
+        )
+    }
+}
+
+private fun snapValue(
+    value: Float,
+    valueRange: ClosedFloatingPointRange<Float>,
+    stepDistance: Float,
+): Float {
+    if (stepDistance == 0f) {
+        return value
+    }
+    val coercedValue = value.coerceIn(valueRange)
+    return Math.round(coercedValue / stepDistance) * stepDistance
+}
+
+@Composable
+private fun AccessibilityParams.createSemantics(
+    value: Float,
+    valueRange: ClosedFloatingPointRange<Float>,
+    onValueChanged: (Float) -> Unit,
+    isEnabled: Boolean,
+    stepDistance: Float,
+): SemanticsPropertyReceiver.() -> Unit {
+    val semanticsContentDescription =
+        disabledMessage
+            ?.takeIf { !isEnabled }
+            ?.let { message ->
+                stringResource(R.string.volume_slider_disabled_message_template, label, message)
+            } ?: label
+    return {
+        contentDescription = semanticsContentDescription
+        if (isEnabled) {
+            currentStateDescription?.let { stateDescription = it }
+            progressBarRangeInfo = ProgressBarRangeInfo(value, valueRange)
+        } else {
+            disabled()
+        }
+        setProgress { targetValue ->
+            val targetDirection =
+                when {
+                    targetValue > value -> 1
+                    targetValue < value -> -1
+                    else -> 0
+                }
+
+            val newValue =
+                (value + targetDirection * stepDistance).coerceIn(
+                    valueRange.start,
+                    valueRange.endInclusive,
+                )
+            onValueChanged(newValue)
+            true
+        }
+    }
+}
+
+@Composable
+private fun Haptics.createViewModel(
+    value: Float,
+    valueRange: ClosedFloatingPointRange<Float>,
+    interactionSource: MutableInteractionSource,
+): SliderHapticsViewModel? {
+    return when (this) {
+        is Haptics.Disabled -> null
+        is Haptics.Enabled -> {
+            hapticsViewModelFactory.let {
+                rememberViewModel(traceName = "SliderHapticsViewModel") {
+                        it.create(
+                            interactionSource,
+                            valueRange,
+                            orientation,
+                            VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
+                                valueRange,
+                                hapticFilter,
+                            ),
+                            VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
+                        )
+                    }
+                    .also { hapticsViewModel ->
+                        var lastDiscreteStep by remember { mutableFloatStateOf(value) }
+                        LaunchedEffect(value) {
+                            snapshotFlow { value }
+                                .map { round(it) }
+                                .filter { it != lastDiscreteStep }
+                                .distinctUntilChanged()
+                                .collect { discreteStep ->
+                                    lastDiscreteStep = discreteStep
+                                    hapticsViewModel.onValueChange(discreteStep)
+                                }
+                        }
+                    }
+            }
+        }
+    }
+}
+
+data class AccessibilityParams(
+    val label: String,
+    val currentStateDescription: String?,
+    val disabledMessage: String?,
+)
+
+sealed interface Haptics {
+    data object Disabled : Haptics
+
+    data class Enabled(
+        val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
+        val hapticFilter: SliderHapticFeedbackFilter,
+        val orientation: Orientation,
+    ) : Haptics
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 88c2697..5c26dac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -1573,6 +1573,25 @@
         assertThat(items.get(1).isFirstDeviceInGroup()).isFalse();
     }
 
+    @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
+    @Test
+    public void deviceListUpdateWithDifferentDevices_firstSelectedDeviceIsFirstDeviceInGroup() {
+        when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
+        doReturn(mMediaDevices)
+                .when(mLocalMediaManager)
+                .getSelectedMediaDevice();
+        mMediaSwitchingController.start(mCb);
+        reset(mCb);
+        mMediaSwitchingController.getMediaItemList().clear();
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+        mMediaDevices.clear();
+        mMediaDevices.add(mMediaDevice2);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+
+        List<MediaItem> items = mMediaSwitchingController.getMediaItemList();
+        assertThat(items.get(0).isFirstDeviceInGroup()).isTrue();
+    }
+
     private int getNumberOfConnectDeviceButtons() {
         int numberOfConnectDeviceButtons = 0;
         for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
index 3d0a8f6..ebbe023 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
@@ -878,4 +878,18 @@
         mMobileDataLayout.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE);
         mConnectedWifi.setVisibility(connectedWifiVisible ? View.VISIBLE : View.GONE);
     }
+
+    @Test
+    public void updateDialog_wifiIsDisabled_turnOffProgressBar() {
+        when(mInternetDetailsContentController.isWifiEnabled()).thenReturn(false);
+        mInternetDialogDelegateLegacy.mIsProgressBarVisible = true;
+
+        mInternetDialogDelegateLegacy.updateDialog(false);
+
+        mBgExecutor.runAllReady();
+        mInternetDialogDelegateLegacy.mDataInternetContent.observe(
+                mInternetDialogDelegateLegacy.mLifecycleOwner, i -> {
+                    assertThat(mInternetDialogDelegateLegacy.mIsProgressBarVisible).isFalse();
+                });
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
index c5b19ab..0b2fea5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
@@ -31,13 +31,13 @@
 import android.view.View
 
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor
 
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.runner.RunWith
 import org.junit.Test
 import org.mockito.Answers
-import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito
@@ -66,11 +66,14 @@
     @Mock
     private lateinit var dialog: ChannelEditorDialog
 
+    private val shadeDialogContextInteractor = FakeShadeDialogContextInteractor(mContext)
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
         `when`(dialogBuilder.build()).thenReturn(dialog)
-        controller = ChannelEditorDialogController(mContext, mockNoMan, dialogBuilder)
+        controller =
+            ChannelEditorDialogController(shadeDialogContextInteractor, mockNoMan, dialogBuilder)
 
         channel1 = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_DEFAULT)
         channel2 = NotificationChannel(TEST_CHANNEL2, TEST_CHANNEL_NAME2, IMPORTANCE_NONE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 24d8d1c..acfa94a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -611,7 +611,8 @@
     public void testCanDismiss_immediately() throws Exception {
         ExpandableNotificationRow row =
                 mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
-        when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(row.getEntry()))
+        when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(
+                row.getEntry().getKey()))
                 .thenReturn(true);
         row.performDismiss(false);
         verify(mNotificationTestHelper.getOnUserInteractionCallback())
@@ -623,7 +624,8 @@
     public void testCanDismiss() throws Exception {
         ExpandableNotificationRow row =
                 mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
-        when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(row.getEntry()))
+        when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(
+                row.getEntry().getKey()))
                 .thenReturn(true);
         row.performDismiss(false);
         TestableLooper.get(this).processAllMessages();
@@ -635,7 +637,8 @@
     public void testCannotDismiss() throws Exception {
         ExpandableNotificationRow row =
                 mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
-        when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(row.getEntry()))
+        when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(
+                row.getEntry().getKey()))
                 .thenReturn(false);
         row.performDismiss(false);
         verify(mNotificationTestHelper.getOnUserInteractionCallback(), never())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
similarity index 96%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 6066a38..9e914ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -65,10 +65,10 @@
             mobileConnectionsRepository.fake.run {
                 setMobileConnectionRepositoryMap(
                     mapOf(
-                        SUB_1_ID to CONNECTION_1,
-                        SUB_2_ID to CONNECTION_2,
-                        SUB_3_ID to CONNECTION_3,
-                        SUB_4_ID to CONNECTION_4,
+                        SUB_1_ID to FakeMobileConnectionRepository(SUB_1_ID, mock()),
+                        SUB_2_ID to FakeMobileConnectionRepository(SUB_2_ID, mock()),
+                        SUB_3_ID to FakeMobileConnectionRepository(SUB_3_ID, mock()),
+                        SUB_4_ID to FakeMobileConnectionRepository(SUB_4_ID, mock()),
                     )
                 )
                 setActiveMobileDataSubscriptionId(SUB_1_ID)
@@ -496,7 +496,10 @@
     @Test
     fun activeDataConnection_turnedOn() =
         kosmos.runTest {
-            CONNECTION_1.setDataEnabled(true)
+            (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
+                    as FakeMobileConnectionRepository)
+                .dataEnabled
+                .value = true
 
             val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
 
@@ -506,10 +509,17 @@
     @Test
     fun activeDataConnection_turnedOff() =
         kosmos.runTest {
-            CONNECTION_1.setDataEnabled(true)
+            (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
+                    as FakeMobileConnectionRepository)
+                .dataEnabled
+                .value = true
+
             val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
 
-            CONNECTION_1.setDataEnabled(false)
+            (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
+                    as FakeMobileConnectionRepository)
+                .dataEnabled
+                .value = false
 
             assertThat(latest).isFalse()
         }
@@ -921,20 +931,18 @@
     @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
     fun isStackable_checksForTerrestrialConnections() =
         kosmos.runTest {
-            val exclusivelyNonTerrestrialSub =
-                SubscriptionModel(
-                    isExclusivelyNonTerrestrial = true,
-                    subscriptionId = 5,
-                    carrierName = "Carrier 5",
-                    profileClass = PROFILE_CLASS_UNSET,
-                )
-
             val latest by collectLastValue(underTest.isStackable)
 
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+            setNumberOfLevelsForSubId(SUB_1_ID, 5)
+            setNumberOfLevelsForSubId(SUB_2_ID, 5)
             assertThat(latest).isTrue()
 
-            connectionsRepository.setSubscriptions(listOf(SUB_1, exclusivelyNonTerrestrialSub))
+            (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
+                    as FakeMobileConnectionRepository)
+                .isNonTerrestrial
+                .value = true
+
             assertThat(latest).isFalse()
         }
 
@@ -1006,7 +1014,6 @@
                 carrierName = "Carrier $SUB_1_ID",
                 profileClass = PROFILE_CLASS_UNSET,
             )
-        private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID, mock())
 
         private const val SUB_2_ID = 2
         private val SUB_2 =
@@ -1015,7 +1022,6 @@
                 carrierName = "Carrier $SUB_2_ID",
                 profileClass = PROFILE_CLASS_UNSET,
             )
-        private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, mock())
 
         private const val SUB_3_ID = 3
         private val SUB_3_OPP =
@@ -1026,7 +1032,6 @@
                 carrierName = "Carrier $SUB_3_ID",
                 profileClass = PROFILE_CLASS_UNSET,
             )
-        private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, mock())
 
         private const val SUB_4_ID = 4
         private val SUB_4_OPP =
@@ -1037,6 +1042,5 @@
                 carrierName = "Carrier $SUB_4_ID",
                 profileClass = PROFILE_CLASS_UNSET,
             )
-        private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, mock())
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
index bc1c60c..c058490 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
@@ -33,7 +33,4 @@
     override suspend fun handleInput(input: QSTileInput<T>) {
         mutex.withLock { mutableInputs.add(input) }
     }
-
-    override var detailsViewModel: TileDetailsViewModel? =
-        FakeTileDetailsViewModel("FakeQSTileUserActionInteractor")
 }
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
index 3ebef02..b616766 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -23,13 +23,10 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.os.Bundle;
 import android.platform.test.annotations.RavenwoodTestRunnerInitializing;
 import android.platform.test.annotations.internal.InnerRunner;
 import android.util.Log;
 
-import androidx.test.platform.app.InstrumentationRegistry;
-
 import com.android.ravenwood.common.RavenwoodCommonUtils;
 
 import org.junit.rules.TestRule;
@@ -285,11 +282,6 @@
     private boolean onBefore(Description description, Scope scope, Order order) {
         Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
 
-        if (scope == Scope.Instance && order == Order.Outer) {
-            // Start of a test method.
-            mState.enterTestMethod(description);
-        }
-
         final var classDescription = getDescription();
 
         // Class-level annotations are checked by the runner already, so we only check
@@ -299,6 +291,12 @@
                 return false;
             }
         }
+
+        if (scope == Scope.Instance && order == Order.Outer) {
+            // Start of a test method.
+            mState.enterTestMethod(description);
+        }
+
         return true;
     }
 
@@ -314,8 +312,7 @@
 
         if (scope == Scope.Instance && order == Order.Outer) {
             // End of a test method.
-            mState.exitTestMethod();
-
+            mState.exitTestMethod(description);
         }
 
         // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
index 70bc52b..705186e 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -81,12 +81,15 @@
         RavenwoodRuntimeEnvironmentController.exitTestClass();
     }
 
+    /** Called when a test method is about to start */
     public void enterTestMethod(Description description) {
         mMethodDescription = description;
-        RavenwoodRuntimeEnvironmentController.initForMethod();
+        RavenwoodRuntimeEnvironmentController.enterTestMethod(description);
     }
 
-    public void exitTestMethod() {
+    /** Called when a test method finishes */
+    public void exitTestMethod(Description description) {
+        RavenwoodRuntimeEnvironmentController.exitTestMethod(description);
         mMethodDescription = null;
     }
 
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index f205d23..d935626 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -51,6 +51,7 @@
 import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.Looper;
+import android.os.Message;
 import android.os.Process_ravenwood;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
@@ -74,6 +75,7 @@
 import com.android.server.LocalServices;
 import com.android.server.compat.PlatformCompat;
 
+import org.junit.AssumptionViolatedException;
 import org.junit.internal.management.ManagementFactory;
 import org.junit.runner.Description;
 
@@ -81,6 +83,7 @@
 import java.io.IOException;
 import java.io.PrintStream;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -93,6 +96,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 /**
  * Responsible for initializing and the environment.
@@ -107,32 +111,60 @@
     @SuppressWarnings("UnusedVariable")
     private static final PrintStream sStdErr = System.err;
 
-    private static final String MAIN_THREAD_NAME = "RavenwoodMain";
+    private static final String MAIN_THREAD_NAME = "Ravenwood:Main";
+    private static final String TESTS_THREAD_NAME = "Ravenwood:Test";
+
     private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
     private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
 
     private static final String ANDROID_LOG_TAGS = "ANDROID_LOG_TAGS";
     private static final String RAVENWOOD_ANDROID_LOG_TAGS = "RAVENWOOD_" + ANDROID_LOG_TAGS;
 
+    static volatile Thread sTestThread;
+    static volatile Thread sMainThread;
+
     /**
      * When enabled, attempt to dump all thread stacks just before we hit the
      * overall Tradefed timeout, to aid in debugging deadlocks.
+     *
+     * Note, this timeout will _not_ stop the test, as there isn't really a clean way to do it.
+     * It'll merely print stacktraces.
      */
     private static final boolean ENABLE_TIMEOUT_STACKS =
-            "1".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
+            !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
 
-    private static final int TIMEOUT_MILLIS = 9_000;
+    private static final boolean TOLERATE_LOOPER_ASSERTS =
+            !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS"));
+
+    static final int DEFAULT_TIMEOUT_SECONDS = 10;
+    private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;
+
+    static int getTimeoutSeconds() {
+        var e = System.getenv("RAVENWOOD_TIMEOUT_SECONDS");
+        if (e == null || e.isEmpty()) {
+            return DEFAULT_TIMEOUT_SECONDS;
+        }
+        return Integer.parseInt(e);
+    }
+
 
     private static final ScheduledExecutorService sTimeoutExecutor =
-            Executors.newScheduledThreadPool(1);
+            Executors.newScheduledThreadPool(1, (Runnable r) -> {
+                Thread t = Executors.defaultThreadFactory().newThread(r);
+                t.setName("Ravenwood:TimeoutMonitor");
+                t.setDaemon(true);
+                return t;
+            });
 
-    private static ScheduledFuture<?> sPendingTimeout;
+    private static volatile ScheduledFuture<?> sPendingTimeout;
 
     /**
      * When enabled, attempt to detect uncaught exceptions from background threads.
      */
     private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION =
-            "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+            !"0".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+
+    private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = true;
 
     /**
      * When set, an unhandled exception was discovered (typically on a background thread), and we
@@ -141,12 +173,6 @@
     private static final AtomicReference<Throwable> sPendingUncaughtException =
             new AtomicReference<>();
 
-    private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
-            (thread, throwable) -> {
-                // Remember the first exception we discover
-                sPendingUncaughtException.compareAndSet(null, throwable);
-            };
-
     // TODO: expose packCallingIdentity function in libbinder and use it directly
     // See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp
     private static long packBinderIdentityToken(
@@ -187,6 +213,8 @@
      * Initialize the global environment.
      */
     public static void globalInitOnce() {
+        sTestThread = Thread.currentThread();
+        Thread.currentThread().setName(TESTS_THREAD_NAME);
         synchronized (sInitializationLock) {
             if (!sInitialized) {
                 // globalInitOnce() is called from class initializer, which cause
@@ -194,6 +222,7 @@
                 sInitialized = true;
 
                 // This is the first call.
+                final long start = System.currentTimeMillis();
                 try {
                     globalInitInner();
                 } catch (Throwable th) {
@@ -202,6 +231,9 @@
                     sExceptionFromGlobalInit = th;
                     SneakyThrow.sneakyThrow(th);
                 }
+                final long end = System.currentTimeMillis();
+                // TODO Show user/system time too
+                Log.e(TAG, "globalInit() took " + (end - start) + "ms");
             } else {
                 // Subsequent calls. If the first call threw, just throw the same error, to prevent
                 // the test from running.
@@ -220,7 +252,8 @@
         RavenwoodCommonUtils.log(TAG, "globalInitInner()");
 
         if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
-            Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
+            Thread.setDefaultUncaughtExceptionHandler(
+                    RavenwoodRuntimeEnvironmentController::reportUncaughtExceptions);
         }
 
         // Some process-wide initialization:
@@ -304,6 +337,7 @@
         ActivityManager.init$ravenwood(SYSTEM.getIdentifier());
 
         final var main = new HandlerThread(MAIN_THREAD_NAME);
+        sMainThread = main;
         main.start();
         Looper.setMainLooperForTest(main.getLooper());
 
@@ -350,9 +384,20 @@
         var systemServerContext =
                 new RavenwoodContext(ANDROID_PACKAGE_NAME, main, systemResourcesLoader);
 
-        sInstrumentation = new Instrumentation();
-        sInstrumentation.basicInit(instContext, targetContext, null);
-        InstrumentationRegistry.registerInstance(sInstrumentation, Bundle.EMPTY);
+        var instArgs = Bundle.EMPTY;
+        RavenwoodUtils.runOnMainThreadSync(() -> {
+            try {
+                // TODO We should get the instrumentation class name from the build file or
+                // somewhere.
+                var InstClass = Class.forName("android.app.Instrumentation");
+                sInstrumentation = (Instrumentation) InstClass.getConstructor().newInstance();
+                sInstrumentation.basicInit(instContext, targetContext, null);
+                sInstrumentation.onCreate(instArgs);
+            } catch (Exception e) {
+                SneakyThrow.sneakyThrow(e);
+            }
+        });
+        InstrumentationRegistry.registerInstance(sInstrumentation, instArgs);
 
         RavenwoodSystemServer.init(systemServerContext);
 
@@ -399,22 +444,46 @@
 
         SystemProperties.clearChangeCallbacksForTest();
 
-        if (ENABLE_TIMEOUT_STACKS) {
-            sPendingTimeout = sTimeoutExecutor.schedule(
-                    RavenwoodRuntimeEnvironmentController::dumpStacks,
-                    TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
-        }
-        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
-            maybeThrowPendingUncaughtException(false);
-        }
+        maybeThrowPendingUncaughtException();
     }
 
     /**
-     * Partially reset and initialize before each test method invocation
+     * Called when a test method is about to be started.
      */
-    public static void initForMethod() {
+    public static void enterTestMethod(Description description) {
         // TODO(b/375272444): this is a hacky workaround to ensure binder identity
         Binder.restoreCallingIdentity(sCallingIdentity);
+
+        scheduleTimeout();
+    }
+
+    /**
+     * Called when a test method finished.
+     */
+    public static void exitTestMethod(Description description) {
+        cancelTimeout();
+        maybeThrowPendingUncaughtException();
+    }
+
+    private static void scheduleTimeout() {
+        if (!ENABLE_TIMEOUT_STACKS) {
+            return;
+        }
+        cancelTimeout();
+
+        sPendingTimeout = sTimeoutExecutor.schedule(
+                RavenwoodRuntimeEnvironmentController::onTestTimedOut,
+                TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+    }
+
+    private static void cancelTimeout() {
+        if (!ENABLE_TIMEOUT_STACKS) {
+            return;
+        }
+        var pt = sPendingTimeout;
+        if (pt != null) {
+            pt.cancel(false);
+        }
     }
 
     private static void initializeCompatIds() {
@@ -473,15 +542,36 @@
     }
 
     /**
+     * Return if an exception is benign and okay to continue running the main looper even
+     * if we detect it.
+     */
+    private static boolean isThrowableBenign(Throwable th) {
+        return th instanceof AssertionError || th instanceof AssumptionViolatedException;
+    }
+
+    static void dispatchMessage(Message msg) {
+        try {
+            msg.getTarget().dispatchMessage(msg);
+        } catch (Throwable th) {
+            var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
+                    Thread.currentThread());
+            sStdErr.println(desc);
+            if (TOLERATE_LOOPER_ASSERTS && isThrowableBenign(th)) {
+                sStdErr.printf("*** Continuing the test because it's %s ***\n",
+                        th.getClass().getSimpleName());
+                var e = new Exception(desc, th);
+                sPendingUncaughtException.compareAndSet(null, e);
+                return;
+            }
+            throw th;
+        }
+    }
+
+    /**
      * A callback when a test class finishes its execution, mostly only for debugging.
      */
     public static void exitTestClass() {
-        if (ENABLE_TIMEOUT_STACKS) {
-            sPendingTimeout.cancel(false);
-        }
-        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
-            maybeThrowPendingUncaughtException(true);
-        }
+        maybeThrowPendingUncaughtException();
     }
 
     public static void logTestRunner(String label, Description description) {
@@ -491,35 +581,70 @@
                 + "(" + description.getTestClass().getName() + ")");
     }
 
-    private static void dumpStacks() {
-        final PrintStream out = System.err;
-        out.println("-----BEGIN ALL THREAD STACKS-----");
-        final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
-        for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
-            out.println();
-            Thread t = stack.getKey();
-            out.println(t.toString() + " ID=" + t.getId());
-            for (StackTraceElement e : stack.getValue()) {
-                out.println("\tat " + e);
-            }
+    private static void maybeThrowPendingUncaughtException() {
+        final Throwable pending = sPendingUncaughtException.getAndSet(null);
+        if (pending != null) {
+            throw new IllegalStateException("Found an uncaught exception", pending);
         }
-        out.println("-----END ALL THREAD STACKS-----");
     }
 
     /**
-     * If there's a pending uncaught exception, consume and throw it now. Typically used to
-     * report an exception on a background thread as a failure for the currently running test.
+     * Prints the stack trace from all threads.
      */
-    private static void maybeThrowPendingUncaughtException(boolean duringReset) {
-        final Throwable pending = sPendingUncaughtException.getAndSet(null);
-        if (pending != null) {
-            if (duringReset) {
-                throw new IllegalStateException(
-                        "Found an uncaught exception during this test", pending);
-            } else {
-                throw new IllegalStateException(
-                        "Found an uncaught exception before this test started", pending);
+    private static void onTestTimedOut() {
+        sStdErr.println("********* SLOW TEST DETECTED ********");
+        dumpStacks(null, null);
+    }
+
+    private static final Object sDumpStackLock = new Object();
+
+    /**
+     * Prints the stack trace from all threads.
+     */
+    private static void dumpStacks(
+            @Nullable Thread exceptionThread, @Nullable Throwable throwable) {
+        cancelTimeout();
+        synchronized (sDumpStackLock) {
+            final PrintStream out = sStdErr;
+            out.println("-----BEGIN ALL THREAD STACKS-----");
+
+            var stacks = Thread.getAllStackTraces();
+            var threads = stacks.keySet().stream().sorted(
+                    Comparator.comparingLong(Thread::getId)).collect(Collectors.toList());
+
+            // Put the test and the main thread at the top.
+            var testThread = sTestThread;
+            var mainThread = sMainThread;
+            if (mainThread != null) {
+                threads.remove(mainThread);
+                threads.add(0, mainThread);
             }
+            if (testThread != null) {
+                threads.remove(testThread);
+                threads.add(0, testThread);
+            }
+            // Put the exception thread at the top.
+            // Also inject the stacktrace from the exception.
+            if (exceptionThread != null) {
+                threads.remove(exceptionThread);
+                threads.add(0, exceptionThread);
+                stacks.put(exceptionThread, throwable.getStackTrace());
+            }
+            for (var th : threads) {
+                out.println();
+
+                out.print("Thread");
+                if (th == exceptionThread) {
+                    out.print(" [** EXCEPTION THREAD **]");
+                }
+                out.print(": " + th.getName() + " / " + th);
+                out.println();
+
+                for (StackTraceElement e :  stacks.get(th)) {
+                    out.println("\tat " + e);
+                }
+            }
+            out.println("-----END ALL THREAD STACKS-----");
         }
     }
 
@@ -545,13 +670,17 @@
                 () -> Class.forName("org.mockito.Matchers"));
     }
 
-    // TODO: use the real UiAutomation class instead of a mock
-    private static UiAutomation createMockUiAutomation() {
-        sAdoptedPermissions = Collections.emptySet();
-        var mock = mock(UiAutomation.class, inv -> {
+    static <T> T makeDefaultThrowMock(Class<T> clazz) {
+        return mock(clazz, inv -> {
             HostTestUtils.onThrowMethodCalled();
             return null;
         });
+    }
+
+    // TODO: use the real UiAutomation class instead of a mock
+    private static UiAutomation createMockUiAutomation() {
+        sAdoptedPermissions = Collections.emptySet();
+        var mock = makeDefaultThrowMock(UiAutomation.class);
         doAnswer(inv -> {
             sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
             return null;
@@ -586,6 +715,23 @@
         }
     }
 
+    private static void reportUncaughtExceptions(Thread th, Throwable e) {
+        sStdErr.printf("Uncaught exception detected: %s: %s\n",
+                th, RavenwoodCommonUtils.getStackTraceString(e));
+
+        doBugreport(th, e, DIE_ON_UNCAUGHT_EXCEPTION);
+    }
+
+    private static void doBugreport(
+            @Nullable Thread exceptionThread, @Nullable Throwable throwable,
+            boolean killSelf) {
+        // TODO: Print more information
+        dumpStacks(exceptionThread, throwable);
+        if (killSelf) {
+            System.exit(13);
+        }
+    }
+
     private static void dumpJavaProperties() {
         Log.v(TAG, "JVM properties:");
         dumpMap(System.getProperties());
@@ -601,7 +747,6 @@
             Log.v(TAG, "  " + key + "=" + map.get(key));
         }
     }
-
     private static void dumpOtherInfo() {
         Log.v(TAG, "Other key information:");
         var jloc = Locale.getDefault();
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index 70c161c1..819d93a 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -26,6 +26,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
@@ -45,6 +46,9 @@
     /** The default values. */
     static final Map<String, String> sDefaultValues = new HashMap<>();
 
+    static final Set<String> sReadableKeys = new HashSet<>();
+    static final Set<String> sWritableKeys = new HashSet<>();
+
     private static final String[] PARTITIONS = {
             "bootimage",
             "odm",
@@ -88,9 +92,24 @@
         ravenwoodProps.forEach((key, origValue) -> {
             final String value;
 
-            // If a value starts with "$$$", then this is a reference to the device-side value.
             if (origValue.startsWith("$$$")) {
+                // If a value starts with "$$$", then:
+                // - If it's "$$$r", the key is allowed to read.
+                // - If it's "$$$w", the key is allowed to write.
+                // - Otherwise, it's a reference to the device-side value.
+                // In case of $$$r and $$$w, if the key ends with a '.', then it'll be treaded
+                // as a prefix match.
                 var deviceKey = origValue.substring(3);
+                if ("r".equals(deviceKey)) {
+                    sReadableKeys.add(key);
+                    Log.v(TAG, key + " (readable)");
+                    return;
+                } else if ("w".equals(deviceKey)) {
+                    sWritableKeys.add(key);
+                    Log.v(TAG, key + " (writable)");
+                    return;
+                }
+
                 var deviceValue = deviceProps.get(deviceKey);
                 if (deviceValue == null) {
                     throw new RuntimeException("Failed to initialize system properties. Key '"
@@ -131,50 +150,38 @@
         sDefaultValues.forEach(RavenwoodRuntimeNative::setSystemProperty);
     }
 
+    private static boolean checkAllowedInner(String key, Set<String> allowed) {
+        if (allowed.contains(key)) {
+            return true;
+        }
+
+        // Also search for a prefix match.
+        for (var k : allowed) {
+            if (k.endsWith(".") && key.startsWith(k)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean checkAllowed(String key, Set<String> allowed) {
+        return checkAllowedInner(key, allowed) || checkAllowedInner(getKeyRoot(key), allowed);
+    }
+
     private static boolean isKeyReadable(String key) {
-        // All writable keys are also readable
-        if (isKeyWritable(key)) return true;
-
-        final String root = getKeyRoot(key);
-
-        // This set is carefully curated to help identify situations where a test may
-        // accidentally depend on a default value of an obscure property whose owner hasn't
-        // decided how Ravenwood should behave.
-        if (root.startsWith("boot.")) return true;
-        if (root.startsWith("build.")) return true;
-        if (root.startsWith("product.")) return true;
-        if (root.startsWith("soc.")) return true;
-        if (root.startsWith("system.")) return true;
-
         // All core values should be readable
-        if (sDefaultValues.containsKey(key)) return true;
-
-        // Hardcoded allowlist
-        return switch (key) {
-            case "gsm.version.baseband",
-                 "no.such.thing",
-                 "qemu.sf.lcd_density",
-                 "ro.bootloader",
-                 "ro.hardware",
-                 "ro.hw_timeout_multiplier",
-                 "ro.odm.build.media_performance_class",
-                 "ro.sf.lcd_density",
-                 "ro.treble.enabled",
-                 "ro.vndk.version",
-                 "ro.icu.data.path" -> true;
-            default -> false;
-        };
+        if (sDefaultValues.containsKey(key)) {
+            return true;
+        }
+        if (checkAllowed(key, sReadableKeys)) {
+            return true;
+        }
+        // All writable keys are also readable
+        return isKeyWritable(key);
     }
 
     private static boolean isKeyWritable(String key) {
-        final String root = getKeyRoot(key);
-
-        if (root.startsWith("debug.")) return true;
-
-        // For PropertyInvalidatedCache
-        if (root.startsWith("cache_key.")) return true;
-
-        return false;
+        return checkAllowed(key, sWritableKeys);
     }
 
     static boolean isKeyAccessible(String key, boolean write) {
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
index 19c1bff..3e2c405 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
@@ -15,7 +15,20 @@
  */
 package android.platform.test.ravenwood;
 
+import static com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod.reflectMethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+
 import com.android.ravenwood.common.RavenwoodCommonUtils;
+import com.android.ravenwood.common.SneakyThrow;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
 
 /**
  * Utilities for writing (bivalent) ravenwood tests.
@@ -47,4 +60,129 @@
     public static void loadJniLibrary(String libname) {
         RavenwoodCommonUtils.loadJniLibrary(libname);
     }
+
+    private class MainHandlerHolder {
+        static Handler sMainHandler = new Handler(Looper.getMainLooper());
+    }
+
+    /**
+     * Returns the main thread handler.
+     */
+    public static Handler getMainHandler() {
+        return MainHandlerHolder.sMainHandler;
+    }
+
+    /**
+     * Run a Callable on Handler and wait for it to complete.
+     */
+    @Nullable
+    public static <T> T runOnHandlerSync(@NonNull Handler h, @NonNull Callable<T> c) {
+        var result = new AtomicReference<T>();
+        var thrown = new AtomicReference<Throwable>();
+        var latch = new CountDownLatch(1);
+        h.post(() -> {
+            try {
+                result.set(c.call());
+            } catch (Throwable th) {
+                thrown.set(th);
+            }
+            latch.countDown();
+        });
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted while waiting on the Runnable", e);
+        }
+        var th = thrown.get();
+        if (th != null) {
+            SneakyThrow.sneakyThrow(th);
+        }
+        return result.get();
+    }
+
+
+    /**
+     * Run a Runnable on Handler and wait for it to complete.
+     */
+    @Nullable
+    public static void runOnHandlerSync(@NonNull Handler h, @NonNull Runnable r) {
+        runOnHandlerSync(h, () -> {
+            r.run();
+            return null;
+        });
+    }
+
+    /**
+     * Run a Callable on main thread and wait for it to complete.
+     */
+    @Nullable
+    public static <T> T runOnMainThreadSync(@NonNull Callable<T> c) {
+        return runOnHandlerSync(getMainHandler(), c);
+    }
+
+    /**
+     * Run a Runnable on main thread and wait for it to complete.
+     */
+    @Nullable
+    public static void runOnMainThreadSync(@NonNull Runnable r) {
+        runOnHandlerSync(getMainHandler(), r);
+    }
+
+    public static class MockitoHelper {
+        private MockitoHelper() {
+        }
+
+        /**
+         * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different
+         * method on. (Maybe we should do it in Ravenizer.)
+         */
+        public static void verifyZeroInteractions(Object... mocks) {
+            if (RavenwoodRule.isOnRavenwood()) {
+                // Mockito 4 or later
+                reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class)
+                        .callStatic(new Object[]{mocks});
+            } else {
+                // Mockito 2
+                reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class)
+                        .callStatic(new Object[]{mocks});
+            }
+        }
+    }
+
+
+    /**
+     * Wrap the given {@link Supplier} to become memoized.
+     *
+     * The underlying {@link Supplier} will only be invoked once, and that result will be cached
+     * and returned for any future requests.
+     */
+    static <T> Supplier<T> memoize(ThrowingSupplier<T> supplier) {
+        return new Supplier<>() {
+            private T mInstance;
+
+            @Override
+            public T get() {
+                synchronized (this) {
+                    if (mInstance == null) {
+                        mInstance = create();
+                    }
+                    return mInstance;
+                }
+            }
+
+            private T create() {
+                try {
+                    return supplier.get();
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        };
+    }
+
+    /** Used by {@link #memoize(ThrowingSupplier)}  */
+    public interface ThrowingSupplier<T> {
+        /** */
+        T get() throws Exception;
+    }
 }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index a967a3f..893b354 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -26,10 +26,12 @@
 import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Member;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.Arrays;
+import java.util.Objects;
 import java.util.function.Supplier;
 
 public class RavenwoodCommonUtils {
@@ -329,4 +331,70 @@
     public static <T> T withDefault(@Nullable T value, @Nullable T def) {
         return value != null ? value : def;
     }
+
+    /**
+     * Utility for calling a method with reflections. Used to call a method by name.
+     * Note, this intentionally does _not_ support non-public methods, as we generally
+     * shouldn't violate java visibility in ravenwood.
+     *
+     * @param <TTHIS> class owning the method.
+     */
+    public static class ReflectedMethod<TTHIS> {
+        private final Class<TTHIS> mThisClass;
+        private final Method mMethod;
+
+        private ReflectedMethod(Class<TTHIS> thisClass, Method method) {
+            mThisClass = thisClass;
+            mMethod = method;
+        }
+
+        /** Factory method. */
+        @SuppressWarnings("unchecked")
+        public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod(
+                @NonNull Class<TTHIS> clazz, @NonNull String methodName,
+                @NonNull Class<?>... argTypes) {
+            try {
+                return new ReflectedMethod(clazz, clazz.getMethod(methodName, argTypes));
+            } catch (NoSuchMethodException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /** Factory method. */
+        @SuppressWarnings("unchecked")
+        public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod(
+                @NonNull String className, @NonNull String methodName,
+                @NonNull Class<?>... argTypes) {
+            try {
+                return reflectMethod((Class<TTHIS>) Class.forName(className), methodName, argTypes);
+            } catch (ClassNotFoundException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /** Call the instance method */
+        @SuppressWarnings("unchecked")
+        public <RET> RET call(@NonNull TTHIS thisObject, @NonNull Object... args) {
+            try {
+                return (RET) mMethod.invoke(Objects.requireNonNull(thisObject), args);
+            } catch (InvocationTargetException | IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /** Call the static method */
+        @SuppressWarnings("unchecked")
+        public <RET> RET callStatic(@NonNull Object... args) {
+            try {
+                return (RET) mMethod.invoke(null, args);
+            } catch (InvocationTargetException | IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    /** Handy method to create an array */
+    public static <T> T[] arr(@NonNull T... objects) {
+        return objects;
+    }
 }
diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
index 7ab9cda..855a4ff 100644
--- a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
@@ -21,7 +21,6 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.RuntimeInit;
 import com.android.ravenwood.RavenwoodRuntimeNative;
-import com.android.ravenwood.common.RavenwoodCommonUtils;
 
 import java.io.PrintStream;
 import java.text.SimpleDateFormat;
@@ -164,7 +163,7 @@
      * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
      * that we don't end up in a recursive loop.
      */
-    private static PrintStream getRealOut() {
+    public static PrintStream getRealOut() {
         if (RuntimeInit.sOut$ravenwood != null) {
             return RuntimeInit.sOut$ravenwood;
         } else {
diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
index eaadac6..50cfd3b 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
@@ -57,4 +57,12 @@
     public int getTargetSdkVersion() {
         return RavenwoodRuntimeState.sTargetSdkLevel;
     }
+
+    /** Ignored on ravenwood. */
+    public void registerNativeAllocation(long bytes) {
+    }
+
+    /** Ignored on ravenwood. */
+    public void registerNativeFree(long bytes) {
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index cf1a513..985e00e 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -97,6 +97,9 @@
         if (referent == null) {
             throw new IllegalArgumentException("referent is null");
         }
+        if (mFreeFunction == 0) {
+            return () -> {}; // do nothing
+        }
         if (nativePtr == 0) {
             throw new IllegalArgumentException("nativePtr is null");
         }
diff --git a/ravenwood/scripts/add-annotations.sh b/ravenwood/scripts/add-annotations.sh
index 3e86037..8c394f5 100755
--- a/ravenwood/scripts/add-annotations.sh
+++ b/ravenwood/scripts/add-annotations.sh
@@ -35,7 +35,7 @@
 # We add this line to each methods found.
 # Note, if we used a single @, that'd be handled as an at file. Use
 # the double-at instead.
-annotation="@@android.platform.test.annotations.DisabledOnRavenwood"
+annotation="@@android.platform.test.annotations.DisabledOnRavenwood(reason = \"bulk-disabled by script\")"
 while getopts "t:" opt; do
 case "$opt" in
     t)
diff --git a/ravenwood/tests/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp
index 9dd7cc6..182a7cf 100644
--- a/ravenwood/tests/coretest/Android.bp
+++ b/ravenwood/tests/coretest/Android.bp
@@ -33,3 +33,34 @@
     },
     auto_gen_config: true,
 }
+
+// Same as RavenwoodCoreTest, but it excludes tests using platform-parametric-runner-lib,
+// because that modules has too many dependencies and slow to build incrementally.
+android_ravenwood_test {
+    name: "RavenwoodCoreTest-light",
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+
+        // This library should be removed by Ravenizer
+        "mockito-target-minus-junit4",
+    ],
+    libs: [
+        // We access internal private classes
+        "ravenwood-junit-impl",
+    ],
+    srcs: [
+        "test/**/*.java",
+        "test/**/*.kt",
+    ],
+
+    exclude_srcs: [
+        "test/com/android/ravenwoodtest/runnercallbacktests/*",
+    ],
+    ravenizer: {
+        strip_mockito: true,
+    },
+    auto_gen_config: true,
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java
new file mode 100644
index 0000000..68387d7
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.ravenwood.RavenwoodUtils;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+public class RavenwoodMainThreadTest {
+    private static final boolean RUN_UNSAFE_TESTS =
+            "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS"));
+
+    @Test
+    public void testRunOnMainThread() {
+        AtomicReference<Thread> thr = new AtomicReference<>();
+        RavenwoodUtils.runOnMainThreadSync(() -> {
+            thr.set(Thread.currentThread());
+        });
+        var th = thr.get();
+        assertThat(th).isNotNull();
+        assertThat(th).isNotEqualTo(Thread.currentThread());
+    }
+
+    /**
+     * Sleep a long time on the main thread. This test would then "pass", but Ravenwood
+     * should show the stack traces.
+     *
+     * This is "unsafe" because this test is slow.
+     */
+    @Test
+    public void testUnsafeMainThreadHang() {
+        assumeTrue(RUN_UNSAFE_TESTS);
+
+        // The test should time out.
+        RavenwoodUtils.runOnMainThreadSync(() -> {
+            try {
+                Thread.sleep(30_000);
+            } catch (InterruptedException e) {
+                fail("Interrupted");
+            }
+        });
+    }
+
+    /**
+     * AssertionError on the main thread would be swallowed and reported "normally".
+     * (Other kinds of exceptions would be caught by the unhandled exception handler, and kills
+     * the process)
+     *
+     * This is "unsafe" only because this feature can be disabled via the env var.
+     */
+    @Test
+    public void testUnsafeAssertFailureOnMainThread() {
+        assumeTrue(RUN_UNSAFE_TESTS);
+
+        assertThrows(AssertionError.class, () -> {
+            RavenwoodUtils.runOnMainThreadSync(() -> {
+                fail();
+            });
+        });
+    }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java
new file mode 100644
index 0000000..421fb50
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link ReflectedMethod}.
+ */
+public class RavenwoodReflectorTest {
+    /** test target */
+    public class Target {
+        private final int mVar;
+
+        /** test target */
+        public Target(int var) {
+            mVar = var;
+        }
+
+        /** test target */
+        public int foo(int x) {
+            return x + mVar;
+        }
+
+        /** test target */
+        public static int bar(int x) {
+            return x + 1;
+        }
+    }
+
+    /** Test for a non-static method call */
+    @Test
+    public void testNonStatic() {
+        var obj = new Target(5);
+
+        var m = ReflectedMethod.reflectMethod(Target.class, "foo", int.class);
+        assertThat((int) m.call(obj, 2)).isEqualTo(7);
+    }
+
+    /** Test for a static method call */
+    @Test
+    public void testStatic() {
+        var m = ReflectedMethod.reflectMethod(Target.class, "bar", int.class);
+        assertThat((int) m.callStatic(1)).isEqualTo(2);
+    }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java
new file mode 100644
index 0000000..454f5a9
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.os.SystemProperties;
+
+import org.junit.Test;
+
+public class RavenwoodSystemPropertiesTest {
+    @Test
+    public void testRead() {
+        assertThat(SystemProperties.get("ro.board.first_api_level")).isEqualTo("1");
+    }
+
+    @Test
+    public void testWrite() {
+        SystemProperties.set("debug.xxx", "5");
+        assertThat(SystemProperties.get("debug.xxx")).isEqualTo("5");
+    }
+
+    private static void assertException(String expectedMessage, Runnable r) {
+        try {
+            r.run();
+            fail("Excepted exception with message '" + expectedMessage + "' but wasn't thrown");
+        } catch (RuntimeException e) {
+            if (e.getMessage().contains(expectedMessage)) {
+                return;
+            }
+            fail("Excepted exception with message '" + expectedMessage + "' but was '"
+                    + e.getMessage() +  "'");
+        }
+    }
+
+
+    @Test
+    public void testReadDisallowed() {
+        assertException("Read access to system property 'nonexisitent' denied", () -> {
+            SystemProperties.get("nonexisitent");
+        });
+    }
+
+    @Test
+    public void testWriteDisallowed() {
+        assertException("failed to set system property \"ro.board.first_api_level\" ", () -> {
+            SystemProperties.set("ro.board.first_api_level", "2");
+        });
+    }
+}
diff --git a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
index 30abaa2..b1a40f0 100644
--- a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
+++ b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
@@ -16,28 +16,27 @@
 package com.android.ravenwoodtest;
 
 import android.platform.test.annotations.IgnoreUnderRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Assert;
-import org.junit.Rule;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RunWith(AndroidJUnit4.class)
 public class RavenwoodMinimumTest {
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
-            .setProcessApp()
-            .build();
-
     @Test
     public void testSimple() {
         Assert.assertTrue(android.os.Process.isApplicationUid(android.os.Process.myUid()));
     }
 
     @Test
+    public void testAssumeNot() {
+        Assume.assumeFalse(android.os.Process.isApplicationUid(android.os.Process.myUid()));
+    }
+
+    @Test
     @IgnoreUnderRavenwood
     public void testIgnored() {
         throw new RuntimeException("Shouldn't be executed under ravenwood");
diff --git a/ravenwood/texts/ravenwood-build.prop b/ravenwood/texts/ravenwood-build.prop
index 37c50f1..512b459 100644
--- a/ravenwood/texts/ravenwood-build.prop
+++ b/ravenwood/texts/ravenwood-build.prop
@@ -8,9 +8,41 @@
 ro.soc.model=Ravenwood
 ro.debuggable=1
 
-# For the graphics stack
-ro.hwui.max_texture_allocation_size=104857600
 persist.sys.locale=en-US
+ro.product.locale=en-US
+
+ro.hwui.max_texture_allocation_size=104857600
+
+# Allowlist control:
+# This set is carefully curated to help identify situations where a test may
+# accidentally depend on a default value of an obscure property whose owner hasn't
+# decided how Ravenwood should behave.
+
+boot.=$$$r
+build.=$$$r
+product.=$$$r
+soc.=$$$r
+system.=$$$r
+wm.debug.=$$$r
+wm.extensions.=$$$r
+
+gsm.version.baseband=$$$r
+no.such.thing=$$$r
+qemu.sf.lcd_density=$$$r
+ro.bootloader=$$$r
+ro.hardware=$$$r
+ro.hw_timeout_multiplier=$$$r
+ro.odm.build.media_performance_class=$$$r
+ro.sf.lcd_density=$$$r
+ro.treble.enabled=$$$r
+ro.vndk.version=$$$r
+ro.icu.data.path=$$$r
+
+# Writable keys
+debug.=$$$w
+
+# For PropertyInvalidatedCache
+cache_key.=$$$w
 
 # The ones starting with "ro.product" or "ro.build" will be copied to all "partitions" too.
 # See RavenwoodSystemProperties.
diff --git a/ravenwood/texts/ravenwood-services-jarjar-rules.txt b/ravenwood/texts/ravenwood-services-jarjar-rules.txt
index 8fdd340..64a0e25 100644
--- a/ravenwood/texts/ravenwood-services-jarjar-rules.txt
+++ b/ravenwood/texts/ravenwood-services-jarjar-rules.txt
@@ -5,7 +5,7 @@
 
 # Rename all other service internals so that tests can continue to statically
 # link services code when owners aren't ready to support on Ravenwood
-rule com.android.server.** repackaged.@0
+rule com.android.server.** repackaged.services.@0
 
 # TODO: support AIDL generated Parcelables via hoststubgen
-rule android.hardware.power.stats.** repackaged.@0
+rule android.hardware.power.stats.** repackaged.services.@0
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index bd34f33..c182c26 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -149,7 +149,6 @@
 
     OperationStorage mOperationStorage;
     List<PackageInfo> mPackages;
-    PackageInfo mCurrentPackage;
     boolean mUpdateSchedule;
     CountDownLatch mLatch;
     FullBackupJob mJob;             // if a scheduled job needs to be finished afterwards
@@ -207,10 +206,9 @@
         for (String pkg : whichPackages) {
             try {
                 PackageManager pm = backupManagerService.getPackageManager();
-                PackageInfo info = pm.getPackageInfoAsUser(pkg,
+                PackageInfo packageInfo = pm.getPackageInfoAsUser(pkg,
                         PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
-                mCurrentPackage = info;
-                if (!mBackupEligibilityRules.appIsEligibleForBackup(info.applicationInfo)) {
+                if (!mBackupEligibilityRules.appIsEligibleForBackup(packageInfo.applicationInfo)) {
                     // Cull any packages that have indicated that backups are not permitted,
                     // that run as system-domain uids but do not define their own backup agents,
                     // as well as any explicit mention of the 'special' shared-storage agent
@@ -220,13 +218,13 @@
                     }
                     mBackupManagerMonitorEventSender.monitorEvent(
                             BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE,
-                            mCurrentPackage,
+                            packageInfo,
                             BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                            null);
+                            /* extras= */ null);
                     BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
                             BackupManager.ERROR_BACKUP_NOT_ALLOWED);
                     continue;
-                } else if (!mBackupEligibilityRules.appGetsFullBackup(info)) {
+                } else if (!mBackupEligibilityRules.appGetsFullBackup(packageInfo)) {
                     // Cull any packages that are found in the queue but now aren't supposed
                     // to get full-data backup operations.
                     if (DEBUG) {
@@ -235,13 +233,13 @@
                     }
                     mBackupManagerMonitorEventSender.monitorEvent(
                             BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT,
-                            mCurrentPackage,
+                            packageInfo,
                             BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                            null);
+                            /* extras= */ null);
                     BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
                             BackupManager.ERROR_BACKUP_NOT_ALLOWED);
                     continue;
-                } else if (mBackupEligibilityRules.appIsStopped(info.applicationInfo)) {
+                } else if (mBackupEligibilityRules.appIsStopped(packageInfo.applicationInfo)) {
                     // Cull any packages in the 'stopped' state: they've either just been
                     // installed or have explicitly been force-stopped by the user.  In both
                     // cases we do not want to launch them for backup.
@@ -250,21 +248,21 @@
                     }
                     mBackupManagerMonitorEventSender.monitorEvent(
                             BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED,
-                            mCurrentPackage,
+                            packageInfo,
                             BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                            null);
+                            /* extras= */ null);
                     BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
                             BackupManager.ERROR_BACKUP_NOT_ALLOWED);
                     continue;
                 }
-                mPackages.add(info);
+                mPackages.add(packageInfo);
             } catch (NameNotFoundException e) {
                 Slog.i(TAG, "Requested package " + pkg + " not found; ignoring");
                 mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND,
-                        mCurrentPackage,
+                        /* pkg= */ null,
                         BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                        null);
+                        /* extras= */ null);
             }
         }
 
@@ -352,10 +350,11 @@
                 } else {
                     monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
                 }
-                mBackupManagerMonitorEventSender
-                        .monitorEvent(monitoringEvent, null,
-                                BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                                null);
+                mBackupManagerMonitorEventSender.monitorEvent(
+                        monitoringEvent,
+                        /* pkg= */ null,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        /* extras= */ null);
                 mUpdateSchedule = false;
                 backupRunStatus = BackupManager.ERROR_BACKUP_NOT_ALLOWED;
                 return;
@@ -367,8 +366,9 @@
                 backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
                 mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT,
-                        mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
-                        null);
+                        /* pkg= */ null,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+                        /* extras= */ null);
                 return;
             }
 
@@ -461,9 +461,10 @@
                         }
                         mBackupManagerMonitorEventSender.monitorEvent(
                                 BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT,
-                                mCurrentPackage,
+                                currentPackage,
                                 BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                                mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+                                BackupManagerMonitorEventSender.putMonitoringExtra(
+                                        /* extras= */ null,
                                         BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR,
                                         preflightResult));
                         backupPackageStatus = (int) preflightResult;
@@ -496,9 +497,9 @@
                                     + ": " + totalRead + " of " + quota);
                             mBackupManagerMonitorEventSender.monitorEvent(
                                     BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT,
-                                    mCurrentPackage,
+                                    currentPackage,
                                     BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
-                                    null);
+                                    /* extras= */ null);
                             mBackupRunner.sendQuotaExceeded(totalRead, quota);
                         }
                     }
@@ -645,9 +646,9 @@
             Slog.w(TAG, "Exception trying full transport backup", e);
             mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP,
-                    mCurrentPackage,
+                    /* pkg= */ null,
                     BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                    mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+                    BackupManagerMonitorEventSender.putMonitoringExtra(/* extras= */ null,
                             BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP,
                             Log.getStackTraceString(e)));
 
@@ -966,9 +967,6 @@
             }
         }
 
-
-        // BackupRestoreTask interface: specifically, timeout detection
-
         @Override
         public void execute() { /* intentionally empty */ }
 
@@ -981,7 +979,9 @@
 
             mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
-                    mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+                    mTarget,
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+                    /* extras= */ null);
             mIsCancelled = true;
             // Cancel tasks spun off by this task.
             mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll);
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
index c4519b1..33668a6 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
@@ -71,6 +71,7 @@
         mMonitor = monitor;
     }
 
+    @Nullable
     public IBackupManagerMonitor getMonitor() {
         return mMonitor;
     }
@@ -87,9 +88,9 @@
      */
     public void monitorEvent(
             int id,
-            PackageInfo pkg,
+            @Nullable PackageInfo pkg,
             int category,
-            Bundle extras) {
+            @Nullable Bundle extras) {
         try {
             Bundle bundle = new Bundle();
             bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id);
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index b6fe0ad..e46bbe2 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -160,6 +160,7 @@
 import com.android.server.pm.Installer;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.storage.AppFuseBridge;
+import com.android.server.storage.ImmutableVolumeInfo;
 import com.android.server.storage.StorageSessionController;
 import com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
 import com.android.server.storage.WatchedVolumeInfo;
@@ -777,7 +778,7 @@
                     break;
                 }
                 case H_VOLUME_UNMOUNT: {
-                    final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
+                    final ImmutableVolumeInfo vol = (ImmutableVolumeInfo) msg.obj;
                     unmount(vol);
                     break;
                 }
@@ -898,8 +899,14 @@
                         for (int i = 0; i < size; i++) {
                             final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                             if (vol.getMountUserId() == userId) {
+                                // Capture the volume before we set mount user id to null,
+                                // so that StorageSessionController remove the session from
+                                // the correct user (old mount user id)
+                                final ImmutableVolumeInfo volToUnmount
+                                        = vol.getClonedImmutableVolumeInfo();
                                 vol.setMountUserId(UserHandle.USER_NULL);
-                                mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
+                                mHandler.obtainMessage(H_VOLUME_UNMOUNT, volToUnmount)
+                                        .sendToTarget();
                             }
                         }
                     }
@@ -1295,7 +1302,12 @@
     }
 
     private void maybeRemountVolumes(int userId) {
-        List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>();
+        // We need to keep 2 lists
+        // 1. List of volumes before we set the mount user Id so that
+        // StorageSessionController is able to remove the session from the correct user (old one)
+        // 2. List of volumes to mount which should have the up to date info
+        List<ImmutableVolumeInfo> volumesToUnmount = new ArrayList<>();
+        List<WatchedVolumeInfo> volumesToMount = new ArrayList<>();
         synchronized (mLock) {
             for (int i = 0; i < mVolumes.size(); i++) {
                 final WatchedVolumeInfo vol = mVolumes.valueAt(i);
@@ -1303,16 +1315,19 @@
                         && vol.getMountUserId() != mCurrentUserId) {
                     // If there's a visible secondary volume mounted,
                     // we need to update the currentUserId and remount
+                    // But capture the volume with the old user id first to use it in unmounting
+                    volumesToUnmount.add(vol.getClonedImmutableVolumeInfo());
                     vol.setMountUserId(mCurrentUserId);
-                    volumesToRemount.add(vol);
+                    volumesToMount.add(vol);
                 }
             }
         }
 
-        for (WatchedVolumeInfo vol : volumesToRemount) {
-            Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol);
-            mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
-            mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
+        for (int i = 0; i < volumesToMount.size(); i++) {
+            Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: "
+                    + volumesToUnmount.get(i));
+            mHandler.obtainMessage(H_VOLUME_UNMOUNT, volumesToUnmount.get(i)).sendToTarget();
+            mHandler.obtainMessage(H_VOLUME_MOUNT, volumesToMount.get(i)).sendToTarget();
         }
     }
 
@@ -2430,10 +2445,10 @@
         super.unmount_enforcePermission();
 
         final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
-        unmount(vol);
+        unmount(vol.getClonedImmutableVolumeInfo());
     }
 
-    private void unmount(WatchedVolumeInfo vol) {
+    private void unmount(ImmutableVolumeInfo vol) {
         try {
             try {
                 if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
@@ -2444,7 +2459,7 @@
             }
             extendWatchdogTimeout("#unmount might be slow");
             mVold.unmount(vol.getId());
-            mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo());
+            mStorageSessionController.onVolumeUnmount(vol);
         } catch (Exception e) {
             Slog.wtf(TAG, e);
         }
diff --git a/services/core/java/com/android/server/SystemTimeZone.java b/services/core/java/com/android/server/SystemTimeZone.java
index dd07081..c8810f6 100644
--- a/services/core/java/com/android/server/SystemTimeZone.java
+++ b/services/core/java/com/android/server/SystemTimeZone.java
@@ -133,6 +133,7 @@
         boolean timeZoneChanged = false;
         synchronized (SystemTimeZone.class) {
             String currentTimeZoneId = getTimeZoneId();
+            @TimeZoneConfidence int currentConfidence = getTimeZoneConfidence();
             if (currentTimeZoneId == null || !currentTimeZoneId.equals(timeZoneId)) {
                 SystemProperties.set(TIME_ZONE_SYSTEM_PROPERTY, timeZoneId);
                 if (DEBUG) {
@@ -145,6 +146,8 @@
                 String logMsg = "Time zone or confidence set: "
                         + " (new) timeZoneId=" + timeZoneId
                         + ", (new) confidence=" + confidence
+                        + ", (old) timeZoneId=" + currentTimeZoneId
+                        + ", (old) confidence=" + currentConfidence
                         + ", logInfo=" + logInfo;
                 addDebugLogEntry(logMsg);
             }
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bd7a0ac..b75b7ddf 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2816,13 +2816,11 @@
         if (!checkNotifyPermission("notifyEmergencyNumberList()")) {
             return;
         }
-        if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
-            if (!mContext.getPackageManager().hasSystemFeature(
-                    PackageManager.FEATURE_TELEPHONY_CALLING)) {
-                // TelephonyManager.getEmergencyNumberList() throws an exception if
-                // FEATURE_TELEPHONY_CALLING is not defined.
-                return;
-            }
+        if (!mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_TELEPHONY_CALLING)) {
+            // TelephonyManager.getEmergencyNumberList() throws an exception if
+            // FEATURE_TELEPHONY_CALLING is not defined.
+            return;
         }
 
         synchronized (mRecords) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8b701f0..b0b34d0 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19471,7 +19471,7 @@
     /**
      * @hide
      */
-    @EnforcePermission("android.permission.INTERACT_ACROSS_USERS_FULL")
+    @EnforcePermission(INTERACT_ACROSS_USERS_FULL)
     public IBinder refreshIntentCreatorToken(Intent intent) {
         refreshIntentCreatorToken_enforcePermission();
         IBinder binder = intent.getCreatorToken();
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index 36035bd..78beb18 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -832,7 +832,9 @@
 
             // If this receiver is going to be skipped, skip it now itself and don't even enqueue
             // it.
-            final String skipReason = mSkipPolicy.shouldSkipMessage(r, receiver);
+            final String skipReason = Flags.avoidNoteOpAtEnqueue()
+                    ? mSkipPolicy.shouldSkipAtEnqueueMessage(r, receiver)
+                    : mSkipPolicy.shouldSkipMessage(r, receiver);
             if (skipReason != null) {
                 setDeliveryState(null, null, r, i, receiver, BroadcastRecord.DELIVERY_SKIPPED,
                         "skipped by policy at enqueue: " + skipReason);
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index d2af84c..b0d5994 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -71,10 +71,20 @@
      *         {@code null} if it can proceed.
      */
     public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) {
+        return shouldSkipMessage(r, target, false /* preflight */);
+    }
+
+    public @Nullable String shouldSkipAtEnqueueMessage(@NonNull BroadcastRecord r,
+            @NonNull Object target) {
+        return shouldSkipMessage(r, target, true /* preflight */);
+    }
+
+    private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target,
+            boolean preflight) {
         if (target instanceof BroadcastFilter) {
-            return shouldSkipMessage(r, (BroadcastFilter) target);
+            return shouldSkipMessage(r, (BroadcastFilter) target, preflight);
         } else {
-            return shouldSkipMessage(r, (ResolveInfo) target);
+            return shouldSkipMessage(r, (ResolveInfo) target, preflight);
         }
     }
 
@@ -86,7 +96,7 @@
      *         {@code null} if it can proceed.
      */
     private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
-            @NonNull ResolveInfo info) {
+            @NonNull ResolveInfo info, boolean preflight) {
         final BroadcastOptions brOptions = r.options;
         final ComponentName component = new ComponentName(
                 info.activityInfo.applicationInfo.packageName,
@@ -134,15 +144,23 @@
                         + " requires " + info.activityInfo.permission;
             }
         } else if (info.activityInfo.permission != null) {
-            final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission);
-            if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode,
-                    r.callingUid, r.callerPackage, r.callerFeatureId,
-                    "Broadcast delivered to " + info.activityInfo.name)
-                    != AppOpsManager.MODE_ALLOWED) {
-                return "Appop Denial: broadcasting "
-                        + broadcastDescription(r, component)
-                        + " requires appop " + AppOpsManager.permissionToOp(
-                                info.activityInfo.permission);
+            final String op = AppOpsManager.permissionToOp(info.activityInfo.permission);
+            if (op != null) {
+                final int mode;
+                if (preflight) {
+                    mode = mService.getAppOpsManager().checkOpNoThrow(op,
+                            r.callingUid, r.callerPackage, r.callerFeatureId);
+                } else {
+                    mode = mService.getAppOpsManager().noteOpNoThrow(op,
+                            r.callingUid, r.callerPackage, r.callerFeatureId,
+                            "Broadcast delivered to " + info.activityInfo.name);
+                }
+                if (mode != AppOpsManager.MODE_ALLOWED) {
+                    return "Appop Denial: broadcasting "
+                            + broadcastDescription(r, component)
+                            + " requires appop " + AppOpsManager.permissionToOp(
+                                    info.activityInfo.permission);
+                }
             }
         }
 
@@ -250,8 +268,8 @@
                     perm = PackageManager.PERMISSION_DENIED;
                 }
 
-                int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
-                if (appOp != AppOpsManager.OP_NONE) {
+                final String appOp = AppOpsManager.permissionToOp(excludedPermission);
+                if (appOp != null) {
                     // When there is an app op associated with the permission,
                     // skip when both the permission and the app op are
                     // granted.
@@ -259,7 +277,7 @@
                                 mService.getAppOpsManager().checkOpNoThrow(appOp,
                                 info.activityInfo.applicationInfo.uid,
                                 info.activityInfo.packageName)
-                            == AppOpsManager.MODE_ALLOWED)) {
+                                        == AppOpsManager.MODE_ALLOWED)) {
                         return "Skipping delivery to " + info.activityInfo.packageName
                                 + " due to excluded permission " + excludedPermission;
                     }
@@ -292,9 +310,10 @@
                     createAttributionSourcesForResolveInfo(info);
             for (int i = 0; i < r.requiredPermissions.length; i++) {
                 String requiredPermission = r.requiredPermissions[i];
-                perm = hasPermissionForDataDelivery(
+                perm = hasPermission(
                         requiredPermission,
                         "Broadcast delivered to " + info.activityInfo.name,
+                        preflight,
                         attributionSources)
                                 ? PackageManager.PERMISSION_GRANTED
                                 : PackageManager.PERMISSION_DENIED;
@@ -308,10 +327,14 @@
                 }
             }
         }
-        if (r.appOp != AppOpsManager.OP_NONE) {
-            if (!noteOpForManifestReceiver(r.appOp, r, info, component)) {
+        if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) {
+            final String op = AppOpsManager.opToPublicName(r.appOp);
+            final boolean appOpAllowed = preflight
+                    ? checkOpForManifestReceiver(r.appOp, op, r, info, component)
+                    : noteOpForManifestReceiver(r.appOp, op, r, info, component);
+            if (!appOpAllowed) {
                 return "Skipping delivery to " + info.activityInfo.packageName
-                        + " due to required appop " + r.appOp;
+                        + " due to required appop " + AppOpsManager.opToName(r.appOp);
             }
         }
 
@@ -338,7 +361,7 @@
      *         {@code null} if it can proceed.
      */
     private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
-            @NonNull BroadcastFilter filter) {
+            @NonNull BroadcastFilter filter, boolean preflight) {
         if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) {
             return "Compat change filtered: broadcasting " + r.intent.toString()
                     + " to uid " + filter.owningUid + " due to compat change "
@@ -372,18 +395,25 @@
                         + " requires " + filter.requiredPermission
                         + " due to registered receiver " + filter;
             } else {
-                final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);
-                if (opCode != AppOpsManager.OP_NONE
-                        && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,
-                        r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")
-                        != AppOpsManager.MODE_ALLOWED) {
-                    return "Appop Denial: broadcasting "
-                            + r.intent.toString()
-                            + " from " + r.callerPackage + " (pid="
-                            + r.callingPid + ", uid=" + r.callingUid + ")"
-                            + " requires appop " + AppOpsManager.permissionToOp(
-                                    filter.requiredPermission)
-                            + " due to registered receiver " + filter;
+                final String op = AppOpsManager.permissionToOp(filter.requiredPermission);
+                if (op != null) {
+                    final int mode;
+                    if (preflight) {
+                        mode = mService.getAppOpsManager().checkOpNoThrow(op,
+                                r.callingUid, r.callerPackage, r.callerFeatureId);
+                    } else {
+                        mode = mService.getAppOpsManager().noteOpNoThrow(op, r.callingUid,
+                                r.callerPackage, r.callerFeatureId,
+                                "Broadcast sent to protected receiver");
+                    }
+                    if (mode != AppOpsManager.MODE_ALLOWED) {
+                        return "Appop Denial: broadcasting "
+                                + r.intent
+                                + " from " + r.callerPackage + " (pid="
+                                + r.callingPid + ", uid=" + r.callingUid + ")"
+                                + " requires appop " + op
+                                + " due to registered receiver " + filter;
+                    }
                 }
             }
         }
@@ -433,9 +463,10 @@
                             .build();
             for (int i = 0; i < r.requiredPermissions.length; i++) {
                 String requiredPermission = r.requiredPermissions[i];
-                final int perm = hasPermissionForDataDelivery(
+                final int perm = hasPermission(
                         requiredPermission,
                         "Broadcast delivered to registered receiver " + filter.receiverId,
+                        preflight,
                         attributionSource)
                                 ? PackageManager.PERMISSION_GRANTED
                                 : PackageManager.PERMISSION_DENIED;
@@ -471,8 +502,8 @@
                 final int perm = checkComponentPermission(excludedPermission,
                         filter.receiverList.pid, filter.receiverList.uid, -1, true);
 
-                int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
-                if (appOp != AppOpsManager.OP_NONE) {
+                final String appOp = AppOpsManager.permissionToOp(excludedPermission);
+                if (appOp != null) {
                     // When there is an app op associated with the permission,
                     // skip when both the permission and the app op are
                     // granted.
@@ -480,14 +511,13 @@
                             mService.getAppOpsManager().checkOpNoThrow(appOp,
                                     filter.receiverList.uid,
                                     filter.packageName)
-                                    == AppOpsManager.MODE_ALLOWED)) {
+                                        == AppOpsManager.MODE_ALLOWED)) {
                         return "Appop Denial: receiving "
-                                + r.intent.toString()
+                                + r.intent
                                 + " to " + filter.receiverList.app
                                 + " (pid=" + filter.receiverList.pid
                                 + ", uid=" + filter.receiverList.uid + ")"
-                                + " excludes appop " + AppOpsManager.permissionToOp(
-                                excludedPermission)
+                                + " excludes appop " + appOp
                                 + " due to sender " + r.callerPackage
                                 + " (uid " + r.callingUid + ")";
                     }
@@ -496,7 +526,7 @@
                     // skip when permission is granted.
                     if (perm == PackageManager.PERMISSION_GRANTED) {
                         return "Permission Denial: receiving "
-                                + r.intent.toString()
+                                + r.intent
                                 + " to " + filter.receiverList.app
                                 + " (pid=" + filter.receiverList.pid
                                 + ", uid=" + filter.receiverList.uid + ")"
@@ -523,19 +553,27 @@
         }
 
         // If the broadcast also requires an app op check that as well.
-        if (r.appOp != AppOpsManager.OP_NONE
-                && mService.getAppOpsManager().noteOpNoThrow(r.appOp,
-                filter.receiverList.uid, filter.packageName, filter.featureId,
-                "Broadcast delivered to registered receiver " + filter.receiverId)
-                != AppOpsManager.MODE_ALLOWED) {
-            return "Appop Denial: receiving "
-                    + r.intent.toString()
-                    + " to " + filter.receiverList.app
-                    + " (pid=" + filter.receiverList.pid
-                    + ", uid=" + filter.receiverList.uid + ")"
-                    + " requires appop " + AppOpsManager.opToName(r.appOp)
-                    + " due to sender " + r.callerPackage
-                    + " (uid " + r.callingUid + ")";
+        if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) {
+            final String op = AppOpsManager.opToPublicName(r.appOp);
+            final int mode;
+            if (preflight) {
+                mode = mService.getAppOpsManager().checkOpNoThrow(op,
+                        filter.receiverList.uid, filter.packageName, filter.featureId);
+            } else {
+                mode = mService.getAppOpsManager().noteOpNoThrow(op,
+                        filter.receiverList.uid, filter.packageName, filter.featureId,
+                        "Broadcast delivered to registered receiver " + filter.receiverId);
+            }
+            if (mode != AppOpsManager.MODE_ALLOWED) {
+                return "Appop Denial: receiving "
+                        + r.intent
+                        + " to " + filter.receiverList.app
+                        + " (pid=" + filter.receiverList.pid
+                        + ", uid=" + filter.receiverList.uid + ")"
+                        + " requires appop " + AppOpsManager.opToName(r.appOp)
+                        + " due to sender " + r.callerPackage
+                        + " (uid " + r.callingUid + ")";
+            }
         }
 
         // Ensure that broadcasts are only sent to other apps if they are explicitly marked as
@@ -572,14 +610,14 @@
                 + ", uid=" + r.callingUid + ") to " + component.flattenToShortString();
     }
 
-    private boolean noteOpForManifestReceiver(int appOp, BroadcastRecord r, ResolveInfo info,
-            ComponentName component) {
+    private boolean noteOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r,
+            ResolveInfo info, ComponentName component) {
         if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) {
-            return noteOpForManifestReceiverInner(appOp, r, info, component, null);
+            return noteOpForManifestReceiverInner(opCode, appOp, r, info, component, null);
         } else {
             // Attribution tags provided, noteOp each tag
             for (String tag : info.activityInfo.attributionTags) {
-                if (!noteOpForManifestReceiverInner(appOp, r, info, component, tag)) {
+                if (!noteOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) {
                     return false;
                 }
             }
@@ -587,8 +625,8 @@
         }
     }
 
-    private boolean noteOpForManifestReceiverInner(int appOp, BroadcastRecord r, ResolveInfo info,
-            ComponentName component, String tag) {
+    private boolean noteOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r,
+            ResolveInfo info, ComponentName component, String tag) {
         if (mService.getAppOpsManager().noteOpNoThrow(appOp,
                     info.activityInfo.applicationInfo.uid,
                     info.activityInfo.packageName,
@@ -598,7 +636,37 @@
             Slog.w(TAG, "Appop Denial: receiving "
                     + r.intent + " to "
                     + component.flattenToShortString()
-                    + " requires appop " + AppOpsManager.opToName(appOp)
+                    + " requires appop " + AppOpsManager.opToName(opCode)
+                    + " due to sender " + r.callerPackage
+                    + " (uid " + r.callingUid + ")");
+            return false;
+        }
+        return true;
+    }
+
+    private boolean checkOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r,
+            ResolveInfo info, ComponentName component) {
+        if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) {
+            return checkOpForManifestReceiverInner(opCode, appOp, r, info, component, null);
+        } else {
+            // Attribution tags provided, noteOp each tag
+            for (String tag : info.activityInfo.attributionTags) {
+                if (!checkOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    private boolean checkOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r,
+            ResolveInfo info, ComponentName component, String tag) {
+        if (mService.getAppOpsManager().checkOpNoThrow(appOp, info.activityInfo.applicationInfo.uid,
+                info.activityInfo.packageName, tag) != AppOpsManager.MODE_ALLOWED) {
+            Slog.w(TAG, "Appop Denial: receiving "
+                    + r.intent + " to "
+                    + component.flattenToShortString()
+                    + " requires appop " + AppOpsManager.opToName(opCode)
                     + " due to sender " + r.callerPackage
                     + " (uid " + r.callingUid + ")");
             return false;
@@ -694,9 +762,10 @@
         return mPermissionManager;
     }
 
-    private boolean hasPermissionForDataDelivery(
+    private boolean hasPermission(
             @NonNull String permission,
             @NonNull String message,
+            boolean preflight,
             @NonNull AttributionSource... attributionSources) {
         final PermissionManager permissionManager = getPermissionManager();
         if (permissionManager == null) {
@@ -704,9 +773,14 @@
         }
 
         for (AttributionSource attributionSource : attributionSources) {
-            final int permissionCheckResult =
-                    permissionManager.checkPermissionForDataDelivery(
-                            permission, attributionSource, message);
+            final int permissionCheckResult;
+            if (preflight) {
+                permissionCheckResult = permissionManager.checkPermissionForPreflight(
+                        permission, attributionSource);
+            } else {
+                permissionCheckResult = permissionManager.checkPermissionForDataDelivery(
+                        permission, attributionSource, message);
+            }
             if (permissionCheckResult != PackageManager.PERMISSION_GRANTED) {
                 return false;
             }
diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
index 7f169db..68e21a3 100644
--- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig
+++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
@@ -15,4 +15,15 @@
     description: "Limit the scope of receiver priorities to within a process"
     is_fixed_read_only: true
     bug: "369487976"
+}
+
+flag {
+    name: "avoid_note_op_at_enqueue"
+    namespace: "backstage_power"
+    description: "Avoid triggering noteOp while enqueueing a broadcast"
+    is_fixed_read_only: true
+    bug: "268016162"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3cb2125..0f1228f 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1164,9 +1164,11 @@
     @GuardedBy("mAccessibilityServiceUidsLock")
     private int[] mAccessibilityServiceUids;
 
-    // Uid of the active input method service to check if caller is the one or not.
-    private int mInputMethodServiceUid = android.os.Process.INVALID_UID;
+    // Input Method
     private final Object mInputMethodServiceUidLock = new Object();
+    // Uid of the active input method service to check if caller is the one or not.
+    @GuardedBy("mInputMethodServiceUidLock")
+    private int mInputMethodServiceUid = android.os.Process.INVALID_UID;
 
     private int mEncodedSurroundMode;
     private String mEnabledSurroundFormats;
@@ -11405,7 +11407,7 @@
 
     /** see {@link AudioManager#getFocusDuckedUidsForTest()} */
     @Override
-    @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+    @EnforcePermission(QUERY_AUDIO_STATE)
     public @NonNull List<Integer> getFocusDuckedUidsForTest() {
         super.getFocusDuckedUidsForTest_enforcePermission();
         return mPlaybackMonitor.getFocusDuckedUids();
@@ -11432,7 +11434,7 @@
      * @see AudioManager#getFocusFadeOutDurationForTest()
      * @return the fade out duration, in ms
      */
-    @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+    @EnforcePermission(QUERY_AUDIO_STATE)
     public long getFocusFadeOutDurationForTest() {
         super.getFocusFadeOutDurationForTest_enforcePermission();
         return mMediaFocusControl.getFocusFadeOutDurationForTest();
@@ -11445,7 +11447,7 @@
      * @return the time gap after a fade out completion on focus loss, and fade in start, in ms
      */
     @Override
-    @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+    @EnforcePermission(QUERY_AUDIO_STATE)
     public long getFocusUnmuteDelayAfterFadeOutForTest() {
         super.getFocusUnmuteDelayAfterFadeOutForTest_enforcePermission();
         return mMediaFocusControl.getFocusUnmuteDelayAfterFadeOutForTest();
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 324f95a..964b97c 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -51,7 +51,6 @@
 import android.view.SurfaceControl;
 
 import com.android.internal.R;
-import com.android.internal.annotations.KeepForWeakReference;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.util.function.pooled.PooledLambda;
@@ -1548,9 +1547,7 @@
     }
 
     public static class Injector {
-        // Ensure the callback is kept to preserve native weak reference lifecycle semantics.
         @SuppressWarnings("unused")
-        @KeepForWeakReference
         private ProxyDisplayEventReceiver mReceiver;
         public void setDisplayEventListenerLocked(Looper looper, DisplayEventListener listener) {
             mReceiver = new ProxyDisplayEventReceiver(looper, listener);
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
index f14cda1..11b2805 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
@@ -96,8 +96,10 @@
             public void onSendCompleted(int error) {
                 if (error != SendMessageResult.SUCCESS) {
                     HdmiLogger.debug("Failed to send <System Audio Mode Request>:" + error);
-                    setSystemAudioMode(false);
-                    finishWithCallback(HdmiControlManager.RESULT_COMMUNICATION_FAILED);
+                    if (error == SendMessageResult.FAIL) {
+                        setSystemAudioMode(false);
+                        finishWithCallback(HdmiControlManager.RESULT_COMMUNICATION_FAILED);
+                    }
                 }
             }
         });
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index fba0b04..ef5babf 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -35,6 +35,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.hardware.display.DisplayManager;
 import android.hardware.input.AidlInputGestureData;
 import android.hardware.input.AidlKeyGestureEvent;
 import android.hardware.input.AppLaunchData;
@@ -57,6 +58,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.view.Display;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
@@ -127,6 +129,7 @@
     private final SettingsObserver mSettingsObserver;
     private final AppLaunchShortcutManager mAppLaunchShortcutManager;
     private final InputGestureManager mInputGestureManager;
+    private final DisplayManager mDisplayManager;
     @GuardedBy("mInputDataStore")
     private final InputDataStore mInputDataStore;
     private static final Object mUserLock = new Object();
@@ -194,6 +197,7 @@
         mSettingsObserver = new SettingsObserver(mHandler);
         mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
         mInputGestureManager = new InputGestureManager(mContext);
+        mDisplayManager = Objects.requireNonNull(mContext.getSystemService(DisplayManager.class));
         mInputDataStore = inputDataStore;
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
         initBehaviors();
@@ -246,12 +250,6 @@
                     new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_DOWN,
                             KeyEvent.KEYCODE_POWER) {
                         @Override
-                        public boolean preCondition() {
-                            return isKeyGestureSupported(
-                                    KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
-                        }
-
-                        @Override
                         public void execute() {
                             handleMultiKeyGesture(
                                     new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER},
@@ -274,12 +272,6 @@
                         new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_POWER,
                                 KeyEvent.KEYCODE_STEM_PRIMARY) {
                             @Override
-                            public boolean preCondition() {
-                                return isKeyGestureSupported(
-                                        KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
-                            }
-
-                            @Override
                             public void execute() {
                                 handleMultiKeyGesture(new int[]{KeyEvent.KEYCODE_POWER,
                                                 KeyEvent.KEYCODE_STEM_PRIMARY},
@@ -333,9 +325,6 @@
                         KeyEvent.KEYCODE_POWER) {
                     @Override
                     public boolean preCondition() {
-                        if (!isKeyGestureSupported(getGestureType())) {
-                            return false;
-                        }
                         switch (mPowerVolUpBehavior) {
                             case POWER_VOLUME_UP_BEHAVIOR_MUTE:
                                 return mRingerToggleChord != Settings.Secure.VOLUME_HUSH_OFF;
@@ -423,12 +412,6 @@
                     new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_BACK,
                             KeyEvent.KEYCODE_DPAD_CENTER) {
                         @Override
-                        public boolean preCondition() {
-                            return isKeyGestureSupported(
-                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
-                        }
-
-                        @Override
                         public void execute() {
                             handleMultiKeyGesture(
                                     new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER},
@@ -468,10 +451,11 @@
         if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
             return false;
         }
-        final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
         if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
                 && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
-            return mKeyCombinationManager.interceptKey(event, interactive);
+            final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
+            final boolean isDefaultDisplayOn = isDefaultDisplayOn();
+            return mKeyCombinationManager.interceptKey(event, interactive && isDefaultDisplayOn);
         }
         return false;
     }
@@ -1038,6 +1022,14 @@
         mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
     }
 
+    private boolean isDefaultDisplayOn() {
+        Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+        if (defaultDisplay == null) {
+            return false;
+        }
+        return Display.isOnState(defaultDisplay.getState());
+    }
+
     @MainThread
     private void notifyKeyGestureEvent(AidlKeyGestureEvent event) {
         InputDevice device = getInputDevice(event.deviceId);
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 7e80cbc..0944a54 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -48,7 +48,6 @@
 import android.util.Slog;
 import android.util.Xml;
 
-import com.android.internal.annotations.KeepForWeakReference;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.FrameworkStatsLog;
@@ -101,7 +100,6 @@
 
     private LocaleManagerBackupHelper mBackupHelper;
 
-    @KeepForWeakReference
     private final PackageMonitor mPackageMonitor;
 
     private final Object mWriteLock = new Object();
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 940bcb4..f40d0dd 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -43,7 +43,10 @@
 import com.android.internal.annotations.GuardedBy;
 
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * A class that represents a broker for the endpoint registered by the client app. This class
@@ -111,6 +114,11 @@
 
         private final boolean mRemoteInitiated;
 
+        /**
+         * The set of seq # for pending reliable messages started by this endpoint for this session.
+         */
+        private final Set<Integer> mPendingSequenceNumbers = new HashSet<>();
+
         SessionInfo(HubEndpointInfo remoteEndpointInfo, boolean remoteInitiated) {
             mRemoteEndpointInfo = remoteEndpointInfo;
             mRemoteInitiated = remoteInitiated;
@@ -131,6 +139,24 @@
         public boolean isActive() {
             return mSessionState == SessionState.ACTIVE;
         }
+
+        public boolean isReliableMessagePending(int sequenceNumber) {
+            return mPendingSequenceNumbers.contains(sequenceNumber);
+        }
+
+        public void setReliableMessagePending(int sequenceNumber) {
+            mPendingSequenceNumbers.add(sequenceNumber);
+        }
+
+        public void setReliableMessageCompleted(int sequenceNumber) {
+            mPendingSequenceNumbers.remove(sequenceNumber);
+        }
+
+        public void forEachPendingReliableMessage(Consumer<Integer> consumer) {
+            for (int sequenceNumber : mPendingSequenceNumbers) {
+                consumer.accept(sequenceNumber);
+            }
+        }
     }
 
     /** A map between a session ID which maps to its current state. */
@@ -208,10 +234,7 @@
             try {
                 mSessionInfoMap.put(sessionId, new SessionInfo(destination, false));
                 mHubInterface.openEndpointSession(
-                        sessionId,
-                        halEndpointInfo.id,
-                        mHalEndpointInfo.id,
-                        serviceDescriptor);
+                        sessionId, halEndpointInfo.id, mHalEndpointInfo.id, serviceDescriptor);
             } catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) {
                 Log.e(TAG, "Exception while calling HAL openEndpointSession", e);
                 cleanupSessionResources(sessionId);
@@ -286,34 +309,42 @@
     public void sendMessage(
             int sessionId, HubMessage message, IContextHubTransactionCallback callback) {
         super.sendMessage_enforcePermission();
-        Message halMessage = ContextHubServiceUtil.createHalMessage(message);
-        if (!isSessionActive(sessionId)) {
-            throw new SecurityException(
-                    "sendMessage called on inactive session (id= " + sessionId + ")");
-        }
-
-        if (callback == null) {
-            try {
-                mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+        synchronized (mOpenSessionLock) {
+            SessionInfo info = mSessionInfoMap.get(sessionId);
+            if (info == null) {
+                throw new IllegalArgumentException(
+                        "sendMessage for invalid session id=" + sessionId);
             }
-        } else {
-            ContextHubServiceTransaction transaction =
-                    mTransactionManager.createSessionMessageTransaction(
-                            mHubInterface, sessionId, halMessage, mPackageName, callback);
-            try {
-                mTransactionManager.addTransaction(transaction);
-            } catch (IllegalStateException e) {
-                Log.e(
-                        TAG,
-                        "Unable to add a transaction in sendMessageToEndpoint "
-                                + "(session ID = "
-                                + sessionId
-                                + ")",
-                        e);
-                transaction.onTransactionComplete(
-                        ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE);
+            if (!info.isActive()) {
+                throw new SecurityException(
+                        "sendMessage called on inactive session (id= " + sessionId + ")");
+            }
+
+            Message halMessage = ContextHubServiceUtil.createHalMessage(message);
+            if (callback == null) {
+                try {
+                    mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+                }
+            } else {
+                ContextHubServiceTransaction transaction =
+                        mTransactionManager.createSessionMessageTransaction(
+                                mHubInterface, sessionId, halMessage, mPackageName, callback);
+                try {
+                    mTransactionManager.addTransaction(transaction);
+                    info.setReliableMessagePending(transaction.getMessageSequenceNumber());
+                } catch (IllegalStateException e) {
+                    Log.e(
+                            TAG,
+                            "Unable to add a transaction in sendMessageToEndpoint "
+                                    + "(session ID = "
+                                    + sessionId
+                                    + ")",
+                            e);
+                    transaction.onTransactionComplete(
+                            ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE);
+                }
             }
         }
     }
@@ -393,7 +424,9 @@
                 int id = mSessionInfoMap.keyAt(i);
                 int count = i + 1;
                 sb.append(
-                        "  " + count + ". id="
+                        "  "
+                                + count
+                                + ". id="
                                 + id
                                 + ", remote:"
                                 + mSessionInfoMap.get(id).getRemoteEndpointInfo());
@@ -461,13 +494,24 @@
     /* package */ void onMessageReceived(int sessionId, HubMessage message) {
         byte code = onMessageReceivedInternal(sessionId, message);
         if (code != ErrorCode.OK && message.isResponseRequired()) {
-            sendMessageDeliveryStatus(
-                    sessionId, message.getMessageSequenceNumber(), code);
+            sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), code);
         }
     }
 
     /* package */ void onMessageDeliveryStatusReceived(
             int sessionId, int sequenceNumber, byte errorCode) {
+        synchronized (mOpenSessionLock) {
+            SessionInfo info = mSessionInfoMap.get(sessionId);
+            if (info == null || !info.isActive()) {
+                Log.w(TAG, "Received delivery status for invalid session: id=" + sessionId);
+                return;
+            }
+            if (!info.isReliableMessagePending(sequenceNumber)) {
+                Log.w(TAG, "Received delivery status for unknown seq: " + sequenceNumber);
+                return;
+            }
+            info.setReliableMessageCompleted(sequenceNumber);
+        }
         mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
     }
 
@@ -492,7 +536,6 @@
                 onCloseEndpointSession(id, Reason.HUB_RESET);
             }
         }
-        // TODO(b/390029594): Cancel any ongoing reliable communication transactions
     }
 
     private Optional<Byte> onEndpointSessionOpenRequestInternal(
@@ -515,9 +558,11 @@
             mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true));
         }
 
-        boolean success = invokeCallback(
-                (consumer) ->
-                        consumer.onSessionOpenRequest(sessionId, initiator, serviceDescriptor));
+        boolean success =
+                invokeCallback(
+                        (consumer) ->
+                                consumer.onSessionOpenRequest(
+                                        sessionId, initiator, serviceDescriptor));
         return success ? Optional.empty() : Optional.of(Reason.UNSPECIFIED);
     }
 
@@ -590,8 +635,15 @@
     private boolean cleanupSessionResources(int sessionId) {
         synchronized (mOpenSessionLock) {
             SessionInfo info = mSessionInfoMap.get(sessionId);
-            if (info != null && !info.isRemoteInitiated()) {
-                mEndpointManager.returnSessionId(sessionId);
+            if (info != null) {
+                if (!info.isRemoteInitiated()) {
+                    mEndpointManager.returnSessionId(sessionId);
+                }
+                info.forEachPendingReliableMessage(
+                        (sequenceNumber) -> {
+                            mTransactionManager.onMessageDeliveryResponse(
+                                    sequenceNumber, /* success= */ false);
+                        });
                 mSessionInfoMap.remove(sessionId);
             }
             return info != null;
@@ -646,10 +698,7 @@
                         try {
                             mWakeLock.release();
                         } catch (RuntimeException e) {
-                            Log.e(
-                                    TAG,
-                                    "Releasing the wakelock for all acquisitions fails - ",
-                                    e);
+                            Log.e(TAG, "Releasing the wakelock for all acquisitions fails - ", e);
                             break;
                         }
                     }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index a430a82..6a1db02 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -29,6 +29,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.time.Duration;
 import java.util.ArrayDeque;
@@ -165,52 +166,61 @@
     /**
      * Creates a transaction for loading a nanoapp.
      *
-     * @param contextHubId       the ID of the hub to load the nanoapp to
-     * @param nanoAppBinary      the binary of the nanoapp to load
+     * @param contextHubId the ID of the hub to load the nanoapp to
+     * @param nanoAppBinary the binary of the nanoapp to load
      * @param onCompleteCallback the client on complete callback
      * @return the generated transaction
      */
     /* package */ ContextHubServiceTransaction createLoadTransaction(
-            int contextHubId, NanoAppBinary nanoAppBinary,
-            IContextHubTransactionCallback onCompleteCallback, String packageName) {
+            int contextHubId,
+            NanoAppBinary nanoAppBinary,
+            IContextHubTransactionCallback onCompleteCallback,
+            String packageName) {
         return new ContextHubServiceTransaction(
-                mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_LOAD_NANOAPP,
-                nanoAppBinary.getNanoAppId(), packageName) {
+                mNextAvailableId.getAndIncrement(),
+                ContextHubTransaction.TYPE_LOAD_NANOAPP,
+                nanoAppBinary.getNanoAppId(),
+                packageName) {
             @Override
-                /* package */ int onTransact() {
+            /* package */ int onTransact() {
                 try {
                     return mContextHubProxy.loadNanoapp(
                             contextHubId, nanoAppBinary, this.getTransactionId());
                 } catch (RemoteException e) {
-                    Log.e(TAG, "RemoteException while trying to load nanoapp with ID 0x" +
-                            Long.toHexString(nanoAppBinary.getNanoAppId()), e);
+                    Log.e(
+                            TAG,
+                            "RemoteException while trying to load nanoapp with ID 0x"
+                                    + Long.toHexString(nanoAppBinary.getNanoAppId()),
+                            e);
                     return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
                 }
             }
 
             @Override
-                /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+            /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                 ContextHubStatsLog.write(
                         ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED,
                         nanoAppBinary.getNanoAppId(),
                         nanoAppBinary.getNanoAppVersion(),
                         ContextHubStatsLog
-                            .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD,
+                                .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD,
                         toStatsTransactionResult(result));
 
-                ContextHubEventLogger.getInstance().logNanoappLoad(
-                        contextHubId,
-                        nanoAppBinary.getNanoAppId(),
-                        nanoAppBinary.getNanoAppVersion(),
-                        nanoAppBinary.getBinary().length,
-                        result == ContextHubTransaction.RESULT_SUCCESS);
+                ContextHubEventLogger.getInstance()
+                        .logNanoappLoad(
+                                contextHubId,
+                                nanoAppBinary.getNanoAppId(),
+                                nanoAppBinary.getNanoAppVersion(),
+                                nanoAppBinary.getBinary().length,
+                                result == ContextHubTransaction.RESULT_SUCCESS);
 
                 if (result == ContextHubTransaction.RESULT_SUCCESS) {
                     // NOTE: The legacy JNI code used to do a query right after a load success
                     // to synchronize the service cache. Instead store the binary that was
                     // requested to load to update the cache later without doing a query.
                     mNanoAppStateManager.addNanoAppInstance(
-                            contextHubId, nanoAppBinary.getNanoAppId(),
+                            contextHubId,
+                            nanoAppBinary.getNanoAppId(),
                             nanoAppBinary.getNanoAppVersion());
                 }
                 try {
@@ -228,42 +238,51 @@
     /**
      * Creates a transaction for unloading a nanoapp.
      *
-     * @param contextHubId       the ID of the hub to unload the nanoapp from
-     * @param nanoAppId          the ID of the nanoapp to unload
+     * @param contextHubId the ID of the hub to unload the nanoapp from
+     * @param nanoAppId the ID of the nanoapp to unload
      * @param onCompleteCallback the client on complete callback
      * @return the generated transaction
      */
     /* package */ ContextHubServiceTransaction createUnloadTransaction(
-            int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+            int contextHubId,
+            long nanoAppId,
+            IContextHubTransactionCallback onCompleteCallback,
             String packageName) {
         return new ContextHubServiceTransaction(
-                mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_UNLOAD_NANOAPP,
-                nanoAppId, packageName) {
+                mNextAvailableId.getAndIncrement(),
+                ContextHubTransaction.TYPE_UNLOAD_NANOAPP,
+                nanoAppId,
+                packageName) {
             @Override
-                /* package */ int onTransact() {
+            /* package */ int onTransact() {
                 try {
                     return mContextHubProxy.unloadNanoapp(
                             contextHubId, nanoAppId, this.getTransactionId());
                 } catch (RemoteException e) {
-                    Log.e(TAG, "RemoteException while trying to unload nanoapp with ID 0x" +
-                            Long.toHexString(nanoAppId), e);
+                    Log.e(
+                            TAG,
+                            "RemoteException while trying to unload nanoapp with ID 0x"
+                                    + Long.toHexString(nanoAppId),
+                            e);
                     return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
                 }
             }
 
             @Override
-                /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+            /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                 ContextHubStatsLog.write(
-                        ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, nanoAppId,
+                        ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED,
+                        nanoAppId,
                         0 /* nanoappVersion */,
                         ContextHubStatsLog
-                            .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD,
+                                .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD,
                         toStatsTransactionResult(result));
 
-                ContextHubEventLogger.getInstance().logNanoappUnload(
-                        contextHubId,
-                        nanoAppId,
-                        result == ContextHubTransaction.RESULT_SUCCESS);
+                ContextHubEventLogger.getInstance()
+                        .logNanoappUnload(
+                                contextHubId,
+                                nanoAppId,
+                                result == ContextHubTransaction.RESULT_SUCCESS);
 
                 if (result == ContextHubTransaction.RESULT_SUCCESS) {
                     mNanoAppStateManager.removeNanoAppInstance(contextHubId, nanoAppId);
@@ -283,31 +302,37 @@
     /**
      * Creates a transaction for enabling a nanoapp.
      *
-     * @param contextHubId       the ID of the hub to enable the nanoapp on
-     * @param nanoAppId          the ID of the nanoapp to enable
+     * @param contextHubId the ID of the hub to enable the nanoapp on
+     * @param nanoAppId the ID of the nanoapp to enable
      * @param onCompleteCallback the client on complete callback
      * @return the generated transaction
      */
     /* package */ ContextHubServiceTransaction createEnableTransaction(
-            int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+            int contextHubId,
+            long nanoAppId,
+            IContextHubTransactionCallback onCompleteCallback,
             String packageName) {
         return new ContextHubServiceTransaction(
-                mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_ENABLE_NANOAPP,
+                mNextAvailableId.getAndIncrement(),
+                ContextHubTransaction.TYPE_ENABLE_NANOAPP,
                 packageName) {
             @Override
-                /* package */ int onTransact() {
+            /* package */ int onTransact() {
                 try {
                     return mContextHubProxy.enableNanoapp(
                             contextHubId, nanoAppId, this.getTransactionId());
                 } catch (RemoteException e) {
-                    Log.e(TAG, "RemoteException while trying to enable nanoapp with ID 0x" +
-                            Long.toHexString(nanoAppId), e);
+                    Log.e(
+                            TAG,
+                            "RemoteException while trying to enable nanoapp with ID 0x"
+                                    + Long.toHexString(nanoAppId),
+                            e);
                     return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
                 }
             }
 
             @Override
-                /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+            /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                 try {
                     onCompleteCallback.onTransactionComplete(result);
                 } catch (RemoteException e) {
@@ -320,31 +345,37 @@
     /**
      * Creates a transaction for disabling a nanoapp.
      *
-     * @param contextHubId       the ID of the hub to disable the nanoapp on
-     * @param nanoAppId          the ID of the nanoapp to disable
+     * @param contextHubId the ID of the hub to disable the nanoapp on
+     * @param nanoAppId the ID of the nanoapp to disable
      * @param onCompleteCallback the client on complete callback
      * @return the generated transaction
      */
     /* package */ ContextHubServiceTransaction createDisableTransaction(
-            int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+            int contextHubId,
+            long nanoAppId,
+            IContextHubTransactionCallback onCompleteCallback,
             String packageName) {
         return new ContextHubServiceTransaction(
-                mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_DISABLE_NANOAPP,
+                mNextAvailableId.getAndIncrement(),
+                ContextHubTransaction.TYPE_DISABLE_NANOAPP,
                 packageName) {
             @Override
-                /* package */ int onTransact() {
+            /* package */ int onTransact() {
                 try {
                     return mContextHubProxy.disableNanoapp(
                             contextHubId, nanoAppId, this.getTransactionId());
                 } catch (RemoteException e) {
-                    Log.e(TAG, "RemoteException while trying to disable nanoapp with ID 0x" +
-                            Long.toHexString(nanoAppId), e);
+                    Log.e(
+                            TAG,
+                            "RemoteException while trying to disable nanoapp with ID 0x"
+                                    + Long.toHexString(nanoAppId),
+                            e);
                     return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
                 }
             }
 
             @Override
-                /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+            /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                 try {
                     onCompleteCallback.onTransactionComplete(result);
                 } catch (RemoteException e) {
@@ -447,18 +478,20 @@
     /**
      * Creates a transaction for querying for a list of nanoapps.
      *
-     * @param contextHubId       the ID of the hub to query
+     * @param contextHubId the ID of the hub to query
      * @param onCompleteCallback the client on complete callback
      * @return the generated transaction
      */
     /* package */ ContextHubServiceTransaction createQueryTransaction(
-            int contextHubId, IContextHubTransactionCallback onCompleteCallback,
+            int contextHubId,
+            IContextHubTransactionCallback onCompleteCallback,
             String packageName) {
         return new ContextHubServiceTransaction(
-                mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_QUERY_NANOAPPS,
+                mNextAvailableId.getAndIncrement(),
+                ContextHubTransaction.TYPE_QUERY_NANOAPPS,
                 packageName) {
             @Override
-                /* package */ int onTransact() {
+            /* package */ int onTransact() {
                 try {
                     return mContextHubProxy.queryNanoapps(contextHubId);
                 } catch (RemoteException e) {
@@ -468,12 +501,12 @@
             }
 
             @Override
-                /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+            /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                 onQueryResponse(result, Collections.emptyList());
             }
 
             @Override
-                /* package */ void onQueryResponse(
+            /* package */ void onQueryResponse(
                     @ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) {
                 try {
                     onCompleteCallback.onQueryResponse(result, nanoAppStateList);
@@ -539,6 +572,14 @@
         }
     }
 
+    @VisibleForTesting
+    /* package */
+    int numReliableMessageTransactionPending() {
+        synchronized (mReliableMessageLock) {
+            return mReliableMessageTransactionMap.size();
+        }
+    }
+
     /**
      * Handles a transaction response from a Context Hub.
      *
@@ -585,18 +626,21 @@
     void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
         if (!Flags.reliableMessageRetrySupportService()) {
             TransactionAcceptConditions conditions =
-                    transaction -> transaction.getTransactionType()
-                            == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
-                    && transaction.getMessageSequenceNumber()
-                            == messageSequenceNumber;
+                    transaction ->
+                            transaction.getTransactionType()
+                                            == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
+                                    && transaction.getMessageSequenceNumber()
+                                            == messageSequenceNumber;
             ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
             if (transaction == null) {
-                Log.w(TAG, "Received unexpected message delivery response (expected"
-                        + " message sequence number = "
-                        + messageSequenceNumber
-                        + ", received messageSequenceNumber = "
-                        + messageSequenceNumber
-                        + ")");
+                Log.w(
+                        TAG,
+                        "Received unexpected message delivery response (expected"
+                                + " message sequence number = "
+                                + messageSequenceNumber
+                                + ", received messageSequenceNumber = "
+                                + messageSequenceNumber
+                                + ")");
                 return;
             }
 
@@ -640,8 +684,10 @@
      */
     /* package */
     void onQueryResponse(List<NanoAppState> nanoAppStateList) {
-        TransactionAcceptConditions conditions = transaction ->
-                transaction.getTransactionType() == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
+        TransactionAcceptConditions conditions =
+                transaction ->
+                        transaction.getTransactionType()
+                                == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
         ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
         if (transaction == null) {
             Log.w(TAG, "Received unexpected query response");
@@ -968,24 +1014,33 @@
     private int toStatsTransactionResult(@ContextHubTransaction.Result int result) {
         switch (result) {
             case ContextHubTransaction.RESULT_SUCCESS:
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS;
             case ContextHubTransaction.RESULT_FAILED_BAD_PARAMS:
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS;
             case ContextHubTransaction.RESULT_FAILED_UNINITIALIZED:
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED;
             case ContextHubTransaction.RESULT_FAILED_BUSY:
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY;
             case ContextHubTransaction.RESULT_FAILED_AT_HUB:
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB;
             case ContextHubTransaction.RESULT_FAILED_TIMEOUT:
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT;
             case ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE:
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
             case ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE:
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE;
             case ContextHubTransaction.RESULT_FAILED_UNKNOWN:
             default: /* fall through */
-                return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN;
+                return ContextHubStatsLog
+                        .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN;
         }
     }
 
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
index 7a96195..9937049 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
 import android.database.sqlite.SQLiteException;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.os.Environment;
@@ -204,6 +205,11 @@
             return false;
         }
         final String clause = WhiteListReportContract.TIMESTAMP + "< " + untilTimestamp;
-        return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+        try {
+            return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+        } catch (SQLiteDatabaseCorruptException e) {
+            Slog.e(TAG, "Error deleting records", e);
+            return false;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 3f2c222..dd52cce 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -46,6 +46,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 public class ConditionProviders extends ManagedServices {
 
@@ -202,7 +203,14 @@
 
     @Override
     protected void loadDefaultsFromConfig() {
-        String defaultDndAccess = mContext.getResources().getString(
+        for (String dndPackage : getDefaultDndAccessPackages(mContext)) {
+            addDefaultComponentOrPackage(dndPackage);
+        }
+    }
+
+    static List<String> getDefaultDndAccessPackages(Context context) {
+        ArrayList<String> packages = new ArrayList<>();
+        String defaultDndAccess = context.getResources().getString(
                 R.string.config_defaultDndAccessPackages);
         if (defaultDndAccess != null) {
             String[] dnds = defaultDndAccess.split(ManagedServices.ENABLED_SERVICES_SEPARATOR);
@@ -210,9 +218,10 @@
                 if (TextUtils.isEmpty(dnds[i])) {
                     continue;
                 }
-                addDefaultComponentOrPackage(dnds[i]);
+                packages.add(dnds[i]);
             }
         }
+        return packages;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index fff812c..0fc182f 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -42,7 +42,6 @@
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
-import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_BUBBLE;
 import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;
 
 import android.annotation.FlaggedApi;
@@ -287,7 +286,7 @@
         if (!TAG_RANKING.equals(tag)) return;
 
         final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1);
-        boolean upgradeForBubbles = xmlVersion >= XML_VERSION_BUBBLES_UPGRADE;
+        boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE;
         boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION);
         if (mShowReviewPermissionsNotification
                 && (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION)) {
@@ -338,19 +337,15 @@
             }
             boolean skipWarningLogged = false;
             boolean skipGroupWarningLogged = false;
-            int bubblePref = parser.getAttributeInt(null, ATT_ALLOW_BUBBLE,
-                    DEFAULT_BUBBLE_PREFERENCE);
-            boolean bubbleLocked = (parser.getAttributeInt(null,
-                    ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS) & USER_LOCKED_BUBBLE)
-                    != 0;
-            if (!bubbleLocked
-                    && upgradeForBubbles
-                    && uid != UNKNOWN_UID
-                    && mAppOps.noteOpNoThrow(OP_SYSTEM_ALERT_WINDOW, uid, name, null,
-                    "check-notif-bubble") == AppOpsManager.MODE_ALLOWED) {
-                // User hasn't changed bubble pref & the app has SAW, so allow all bubbles.
-                bubblePref = BUBBLE_PREFERENCE_ALL;
+            boolean hasSAWPermission = false;
+            if (upgradeForBubbles && uid != UNKNOWN_UID) {
+                hasSAWPermission = mAppOps.noteOpNoThrow(
+                        OP_SYSTEM_ALERT_WINDOW, uid, name, null,
+                        "check-notif-bubble") == AppOpsManager.MODE_ALLOWED;
             }
+            int bubblePref = hasSAWPermission
+                    ? BUBBLE_PREFERENCE_ALL
+                    : parser.getAttributeInt(null, ATT_ALLOW_BUBBLE, DEFAULT_BUBBLE_PREFERENCE);
             int appImportance = parser.getAttributeInt(null, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
 
             // when data is loaded from disk it's loaded as USER_ALL, but restored data that
diff --git a/services/core/java/com/android/server/notification/ZenConfigTrimmer.java b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java
new file mode 100644
index 0000000..d65954d
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java
@@ -0,0 +1,109 @@
+/*
+ * 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.notification;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenModeConfig;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+class ZenConfigTrimmer {
+
+    private static final String TAG = "ZenConfigTrimmer";
+    private static final int MAXIMUM_PARCELED_SIZE = 150_000; // bytes
+
+    private final HashSet<String> mTrustedPackages;
+
+    ZenConfigTrimmer(Context context) {
+        mTrustedPackages = new HashSet<>();
+        mTrustedPackages.add(SystemZenRules.PACKAGE_ANDROID);
+        mTrustedPackages.addAll(ConditionProviders.getDefaultDndAccessPackages(context));
+    }
+
+    void trimToMaximumSize(ZenModeConfig config) {
+        Map<String, PackageRules> rulesPerPackage = new HashMap<>();
+        for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) {
+            PackageRules pkgRules = rulesPerPackage.computeIfAbsent(rule.pkg, PackageRules::new);
+            pkgRules.mRules.add(rule);
+        }
+
+        int totalSize = 0;
+        for (PackageRules pkgRules : rulesPerPackage.values()) {
+            totalSize += pkgRules.dataSize();
+        }
+
+        if (totalSize > MAXIMUM_PARCELED_SIZE) {
+            List<PackageRules> deletionCandidates = new ArrayList<>();
+            for (PackageRules pkgRules : rulesPerPackage.values()) {
+                if (!mTrustedPackages.contains(pkgRules.mPkg)) {
+                    deletionCandidates.add(pkgRules);
+                }
+            }
+            deletionCandidates.sort(Comparator.comparingInt(PackageRules::dataSize).reversed());
+
+            evictPackagesFromConfig(config, deletionCandidates, totalSize);
+        }
+    }
+
+    private static void evictPackagesFromConfig(ZenModeConfig config,
+            List<PackageRules> deletionCandidates, int currentSize) {
+        while (currentSize > MAXIMUM_PARCELED_SIZE && !deletionCandidates.isEmpty()) {
+            PackageRules rulesToDelete = deletionCandidates.removeFirst();
+            Slog.w(TAG, String.format("Evicting %s zen rules from package '%s' (%s bytes)",
+                    rulesToDelete.mRules.size(), rulesToDelete.mPkg, rulesToDelete.dataSize()));
+
+            for (ZenModeConfig.ZenRule rule : rulesToDelete.mRules) {
+                config.automaticRules.remove(rule.id);
+            }
+
+            currentSize -= rulesToDelete.dataSize();
+        }
+    }
+
+    private static class PackageRules {
+        private final String mPkg;
+        private final List<ZenModeConfig.ZenRule> mRules;
+        private int mParceledSize = -1;
+
+        PackageRules(String pkg) {
+            mPkg = pkg;
+            mRules = new ArrayList<>();
+        }
+
+        private int dataSize() {
+            if (mParceledSize >= 0) {
+                return mParceledSize;
+            }
+            Parcel parcel = Parcel.obtain();
+            try {
+                parcel.writeParcelableList(mRules, 0);
+                mParceledSize = parcel.dataSize();
+                return mParceledSize;
+            } finally {
+                parcel.recycle();
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 889df51..8b09c2a 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -48,6 +48,7 @@
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.server.notification.Flags.preventZenDeviceEffectsWhileDriving;
+import static com.android.server.notification.Flags.limitZenConfigSize;
 
 import static java.util.Objects.requireNonNull;
 
@@ -192,6 +193,7 @@
     private final ConditionProviders.Config mServiceConfig;
     private final SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver;
     private final ZenModeEventLogger mZenModeEventLogger;
+    private final ZenConfigTrimmer mConfigTrimmer;
 
     @VisibleForTesting protected int mZenMode;
     @VisibleForTesting protected NotificationManager.Policy mConsolidatedPolicy;
@@ -226,6 +228,7 @@
         mClock = clock;
         addCallback(mMetrics);
         mAppOps = context.getSystemService(AppOpsManager.class);
+        mConfigTrimmer = new ZenConfigTrimmer(mContext);
 
         mDefaultConfig = Flags.modesUi()
                 ? ZenModeConfig.getDefaultConfig()
@@ -2061,20 +2064,20 @@
                 Log.w(TAG, "Invalid config in setConfigLocked; " + config);
                 return false;
             }
+            if (limitZenConfigSize() && (origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP)) {
+                mConfigTrimmer.trimToMaximumSize(config);
+            }
+
             if (config.user != mUser) {
                 // simply store away for background users
-                synchronized (mConfigLock) {
-                    mConfigs.put(config.user, config);
-                }
+                mConfigs.put(config.user, config);
                 if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user);
                 return true;
             }
             // handle CPS backed conditions - danger! may modify config
             mConditions.evaluateConfig(config, null, false /*processSubscriptions*/);
 
-            synchronized (mConfigLock) {
-                mConfigs.put(config.user, config);
-            }
+            mConfigs.put(config.user, config);
             if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable());
             ZenLog.traceConfig(origin, reason, triggeringComponent, mConfig, config, callingUid);
 
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 76cd5c8..346d65a 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -212,6 +212,16 @@
 }
 
 flag {
+  name: "limit_zen_config_size"
+  namespace: "systemui"
+  description: "Enforce a maximum (serialized) size for the Zen configuration"
+  bug: "387498139"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "managed_services_concurrent_multiuser"
   namespace: "systemui"
   description: "Enables ManagedServices to support Concurrent multi user environment"
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index e613700..8d787fe 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -82,7 +82,6 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.KeepForWeakReference;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.content.om.OverlayConfig;
 import com.android.internal.util.ArrayUtils;
@@ -267,7 +266,6 @@
 
     private final OverlayActorEnforcer mActorEnforcer;
 
-    @KeepForWeakReference
     private final PackageMonitor mPackageMonitor = new OverlayManagerPackageMonitor();
 
     private int mPrevStartedUserId = -1;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 80c2d41..5a6d7a2 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2937,28 +2937,20 @@
 
         int flags = UserManager.SWITCHABILITY_STATUS_OK;
 
-        t.traceBegin("TM.isInCall");
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
-            if (com.android.internal.telephony.flags
-                    .Flags.enforceTelephonyFeatureMappingForPublicApis()) {
-                if (mContext.getPackageManager().hasSystemFeature(
-                        PackageManager.FEATURE_TELECOM)) {
-                    if (telecomManager != null && telecomManager.isInCall()) {
-                        flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
-                    }
-                }
-            } else {
+        if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) {
+            t.traceBegin("TM.isInCall");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                final TelecomManager telecomManager = mContext.getSystemService(
+                        TelecomManager.class);
                 if (telecomManager != null && telecomManager.isInCall()) {
                     flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
                 }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
             }
-        } finally {
-            Binder.restoreCallingIdentity(identity);
+            t.traceEnd();
         }
-        t.traceEnd();
-
         t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH");
         if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) {
             flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
diff --git a/services/core/java/com/android/server/policy/KeyCombinationManager.java b/services/core/java/com/android/server/policy/KeyCombinationManager.java
index 1592ef3..1b98dd1 100644
--- a/services/core/java/com/android/server/policy/KeyCombinationManager.java
+++ b/services/core/java/com/android/server/policy/KeyCombinationManager.java
@@ -148,19 +148,19 @@
      * to a window.
      * Return true if any active rule could be triggered by the key event, otherwise false.
      */
-    public boolean interceptKey(KeyEvent event, boolean interactive) {
+    public boolean interceptKey(KeyEvent event, boolean isDefaultDisplayInteractive) {
         synchronized (mLock) {
-            return interceptKeyLocked(event, interactive);
+            return interceptKeyLocked(event, isDefaultDisplayInteractive);
         }
     }
 
-    private boolean interceptKeyLocked(KeyEvent event, boolean interactive) {
+    private boolean interceptKeyLocked(KeyEvent event, boolean isDefaultDisplayInteractive) {
         final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
         final int keyCode = event.getKeyCode();
         final int count = mActiveRules.size();
         final long eventTime = event.getEventTime();
 
-        if (interactive && down) {
+        if (isDefaultDisplayInteractive && down) {
             if (mDownTimes.size() > 0) {
                 if (count > 0
                         && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index d11f5e7..f27194a 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4283,22 +4283,19 @@
                     case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
                     case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
                     case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
-                        return true;
                     case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
                     case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
                     case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
                     case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
-                        return mDefaultDisplayPolicy.isAwake();
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
-                        return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
-                                .isAccessibilityShortcutAvailable(isKeyguardLocked());
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
-                        return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
-                                .isAccessibilityShortcutAvailable(false);
                     case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
-                        return enableTalkbackAndMagnifierKeyGestures();
                     case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
-                        return enableVoiceAccessKeyGestures();
+                        return true;
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+                        return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+                                isKeyguardLocked());
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+                        return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+                                false);
                     default:
                         return false;
                 }
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 16658e3..a64e38e 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -131,7 +131,6 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.KeepForWeakReference;
 import com.android.internal.camera.flags.Flags;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.os.BackgroundThread;
@@ -2008,11 +2007,7 @@
     }
 
     private class CallStateHelper {
-        // TelephonyCallback instances are only weakly referenced when registered, so we need
-        // to ensure these fields are kept during optimization to preserve lifecycle semantics.
-        @KeepForWeakReference
         private final OutgoingEmergencyStateCallback mEmergencyStateCallback;
-        @KeepForWeakReference
         private final CallStateCallback mCallStateCallback;
 
         private boolean mIsInEmergencyCall;
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index fab19b6..1afbb34 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -160,8 +160,10 @@
      * @param displayId The changed display Id.
      * @param rootDisplayAreaId The changed display area Id.
      * @param isImmersiveMode {@code true} if the display area get into immersive mode.
+     * @param windowType The window type of the controlling window.
      */
-    void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode);
+    void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode,
+            int windowType);
 
     /**
      * Show a rotation suggestion that a user may approve to rotate the screen.
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index da9d016..798c794 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -732,7 +732,7 @@
 
         @Override
         public void immersiveModeChanged(int displayId, int rootDisplayAreaId,
-                boolean isImmersiveMode) {
+                boolean isImmersiveMode, int windowType) {
             if (mBar == null) {
                 return;
             }
@@ -746,7 +746,7 @@
             if (!CLIENT_TRANSIENT) {
                 // Only call from here when the client transient is not enabled.
                 try {
-                    mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+                    mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode, windowType);
                 } catch (RemoteException ex) {
                 }
             }
diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
index 94e52cd..d4b20fb 100644
--- a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
+++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
@@ -68,6 +68,10 @@
         return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo);
     }
 
+    public ImmutableVolumeInfo getClonedImmutableVolumeInfo() {
+        return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo.clone());
+    }
+
     public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
         return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted);
     }
diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
index bda3d44..621a128 100644
--- a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
@@ -51,6 +51,7 @@
 final class VendorVibrationSession extends IVibrationSession.Stub
         implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient {
     private static final String TAG = "VendorVibrationSession";
+    private static final boolean DEBUG = false;
 
     /** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */
     interface VibratorManagerHooks {
@@ -73,8 +74,8 @@
     private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport();
     private final int[] mVibratorIds;
     private final long mCreateUptime;
-    private final long mCreateTime; // for debugging
-    private final IVibrationSessionCallback mCallback;
+    private final long mCreateTime;
+    private final VendorCallbackWrapper mCallback;
     private final CallerInfo mCallerInfo;
     private final VibratorManagerHooks mManagerHooks;
     private final DeviceAdapter mDeviceAdapter;
@@ -88,11 +89,11 @@
     @GuardedBy("mLock")
     private boolean mEndedByVendor;
     @GuardedBy("mLock")
-    private long mStartTime; // for debugging
+    private long mStartTime;
     @GuardedBy("mLock")
     private long mEndUptime;
     @GuardedBy("mLock")
-    private long mEndTime; // for debugging
+    private long mEndTime;
     @GuardedBy("mLock")
     private VibrationStepConductor mConductor;
 
@@ -103,7 +104,7 @@
         mCreateTime = System.currentTimeMillis();
         mVibratorIds = deviceAdapter.getAvailableVibratorIds();
         mHandler = handler;
-        mCallback = callback;
+        mCallback = new VendorCallbackWrapper(callback, handler);
         mCallerInfo = callerInfo;
         mManagerHooks = managerHooks;
         mDeviceAdapter = deviceAdapter;
@@ -119,7 +120,9 @@
 
     @Override
     public void finishSession() {
-        Slog.d(TAG, "Session finish requested, ending vibration session...");
+        if (DEBUG) {
+            Slog.d(TAG, "Session finish requested, ending vibration session...");
+        }
         // Do not abort session in HAL, wait for ongoing vibration requests to complete.
         // This might take a while to end the session, but it can be aborted by cancelSession.
         requestEndSession(Status.FINISHED, /* shouldAbort= */ false, /* isVendorRequest= */ true);
@@ -127,7 +130,9 @@
 
     @Override
     public void cancelSession() {
-        Slog.d(TAG, "Session cancel requested, aborting vibration session...");
+        if (DEBUG) {
+            Slog.d(TAG, "Session cancel requested, aborting vibration session...");
+        }
         // Always abort session in HAL while cancelling it.
         // This might be triggered after finishSession was already called.
         requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true,
@@ -156,7 +161,7 @@
 
     @Override
     public IBinder getCallerToken() {
-        return mCallback.asBinder();
+        return mCallback.getBinderToken();
     }
 
     @Override
@@ -176,36 +181,30 @@
 
     @Override
     public void onCancel() {
-        Slog.d(TAG, "Session cancellation signal received, aborting vibration session...");
+        if (DEBUG) {
+            Slog.d(TAG, "Session cancellation signal received, aborting vibration session...");
+        }
         requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true,
                 /* isVendorRequest= */ true);
     }
 
     @Override
     public void binderDied() {
-        Slog.d(TAG, "Session binder died, aborting vibration session...");
+        if (DEBUG) {
+            Slog.d(TAG, "Session binder died, aborting vibration session...");
+        }
         requestEndSession(Status.CANCELLED_BINDER_DIED, /* shouldAbort= */ true,
                 /* isVendorRequest= */ false);
     }
 
     @Override
     public boolean linkToDeath() {
-        try {
-            mCallback.asBinder().linkToDeath(this, 0);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Error linking session to token death", e);
-            return false;
-        }
-        return true;
+        return mCallback.linkToDeath(this);
     }
 
     @Override
     public void unlinkToDeath() {
-        try {
-            mCallback.asBinder().unlinkToDeath(this, 0);
-        } catch (NoSuchElementException e) {
-            Slog.wtf(TAG, "Failed to unlink session to token death", e);
-        }
+        mCallback.unlinkToDeath(this);
     }
 
     @Override
@@ -219,26 +218,37 @@
 
     @Override
     public void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId) {
-        Slog.d(TAG, "Vibration callback received for vibration " + vibrationId + " step " + stepId
-                + " on vibrator " + vibratorId + ", ignoring...");
+        if (DEBUG) {
+            Slog.d(TAG, "Vibration callback received for vibration " + vibrationId
+                    + " step " + stepId + " on vibrator " + vibratorId + ", ignoring...");
+        }
     }
 
     @Override
     public void notifySyncedVibratorsCallback(long vibrationId) {
-        Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId
-                + ", ignoring...");
+        if (DEBUG) {
+            Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId
+                    + ", ignoring...");
+        }
     }
 
     @Override
     public void notifySessionCallback() {
-        Slog.d(TAG, "Session callback received, ending vibration session...");
+        if (DEBUG) {
+            Slog.d(TAG, "Session callback received, ending vibration session...");
+        }
         synchronized (mLock) {
             // If end was not requested then the HAL has cancelled the session.
-            maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON,
+            notifyEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON,
                     /* isVendorRequest= */ false);
             maybeSetStatusToRequestedLocked();
             clearVibrationConductor();
-            mHandler.post(() -> mManagerHooks.onSessionReleased(mSessionId));
+            final Status endStatus = mStatus;
+            mHandler.post(() -> {
+                mManagerHooks.onSessionReleased(mSessionId);
+                // Only trigger client callback after session is released in the manager.
+                mCallback.notifyFinished(endStatus);
+            });
         }
     }
 
@@ -271,7 +281,7 @@
 
     public boolean isEnded() {
         synchronized (mLock) {
-            return mStatus != Status.RUNNING;
+            return mEndTime > 0;
         }
     }
 
@@ -297,19 +307,17 @@
                 // Session already ended, skip start callbacks.
                 isAlreadyEnded = true;
             } else {
+                if (DEBUG) {
+                    Slog.d(TAG, "Session started at the HAL");
+                }
                 mStartTime = System.currentTimeMillis();
-                // Run client callback in separate thread.
-                mHandler.post(() -> {
-                    try {
-                        mCallback.onStarted(this);
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "Error notifying vendor session started", e);
-                    }
-                });
+                mCallback.notifyStarted(this);
             }
         }
         if (isAlreadyEnded) {
-            Slog.d(TAG, "Session already ended after starting the HAL, aborting...");
+            if (DEBUG) {
+                Slog.d(TAG, "Session already ended after starting the HAL, aborting...");
+            }
             mHandler.post(() -> mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true));
         }
     }
@@ -337,8 +345,10 @@
     public boolean maybeSetVibrationConductor(VibrationStepConductor conductor) {
         synchronized (mLock) {
             if (mConductor != null) {
-                Slog.d(TAG, "Session still dispatching previous vibration, new vibration "
-                        + conductor.getVibration().id + " ignored");
+                if (DEBUG) {
+                    Slog.d(TAG, "Session still dispatching previous vibration, new vibration "
+                            + conductor.getVibration().id + " ignored");
+                }
                 return false;
             }
             mConductor = conductor;
@@ -347,53 +357,45 @@
     }
 
     private void requestEndSession(Status status, boolean shouldAbort, boolean isVendorRequest) {
-        Slog.d(TAG, "Session end request received with status " + status);
-        boolean shouldTriggerSessionHook = false;
+        if (DEBUG) {
+            Slog.d(TAG, "Session end request received with status " + status);
+        }
         synchronized (mLock) {
-            maybeSetEndRequestLocked(status, isVendorRequest);
+            notifyEndRequestLocked(status, isVendorRequest);
             if (!isEnded() && isStarted()) {
                 // Trigger session hook even if it was already triggered, in case a second request
                 // is aborting the ongoing/ending session. This might cause it to end right away.
                 // Wait for HAL callback before setting the end status.
-                shouldTriggerSessionHook = true;
+                if (DEBUG) {
+                    Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort);
+                }
+                mHandler.post(() ->  mManagerHooks.endSession(mSessionId, shouldAbort));
             } else {
-                // Session not active in the HAL, set end status right away.
+                // Session not active in the HAL, try to set end status right away.
                 maybeSetStatusToRequestedLocked();
+                // Use status used to end this session, which might be different from requested.
+                mCallback.notifyFinished(mStatus);
             }
         }
-        if (shouldTriggerSessionHook) {
-            Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort);
-            mHandler.post(() ->  mManagerHooks.endSession(mSessionId, shouldAbort));
-        }
     }
 
     @GuardedBy("mLock")
-    private void maybeSetEndRequestLocked(Status status, boolean isVendorRequest) {
+    private void notifyEndRequestLocked(Status status, boolean isVendorRequest) {
         if (mEndStatusRequest != null) {
-            // End already requested, keep first requested status and time.
+            // End already requested, keep first requested status.
             return;
         }
-        Slog.d(TAG, "Session end request accepted for status " + status);
+        if (DEBUG) {
+            Slog.d(TAG, "Session end request accepted for status " + status);
+        }
         mEndStatusRequest = status;
         mEndedByVendor = isVendorRequest;
-        mEndTime = System.currentTimeMillis();
-        mEndUptime = SystemClock.uptimeMillis();
+        mCallback.notifyFinishing();
         if (mConductor != null) {
             // Vibration is being dispatched when session end was requested, cancel it.
             mConductor.notifyCancelled(new Vibration.EndInfo(status),
                     /* immediate= */ status != Status.FINISHED);
         }
-        if (isStarted()) {
-            // Only trigger "finishing" callback if session started.
-            // Run client callback in separate thread.
-            mHandler.post(() -> {
-                try {
-                    mCallback.onFinishing();
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Error notifying vendor session is finishing", e);
-                }
-            });
-        }
     }
 
     @GuardedBy("mLock")
@@ -406,40 +408,123 @@
             // No end status was requested, nothing to set.
             return;
         }
-        Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest);
+        if (DEBUG) {
+            Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest);
+        }
         mStatus = mEndStatusRequest;
-        // Run client callback in separate thread.
-        final Status endStatus = mStatus;
-        mHandler.post(() -> {
-            try {
-                mCallback.onFinished(toSessionStatus(endStatus));
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Error notifying vendor session finished", e);
-            }
-        });
+        mEndTime = System.currentTimeMillis();
+        mEndUptime = SystemClock.uptimeMillis();
     }
 
-    @android.os.vibrator.VendorVibrationSession.Status
-    private static int toSessionStatus(Status status) {
-        // Exhaustive switch to cover all possible internal status.
-        return switch (status) {
-            case FINISHED
-                    -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
-            case IGNORED_UNSUPPORTED
-                    -> STATUS_UNSUPPORTED;
-            case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
-                 CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
-                 CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
-                    -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
-            case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
-                 IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
-                 IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
-                 IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
-                    -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
-            case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING,
-                 IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING
-                    -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
-        };
+    /**
+     * Wrapper class to handle client callbacks asynchronously.
+     *
+     * <p>This class is also responsible for link/unlink to the client process binder death, and for
+     * making sure the callbacks are only triggered once. The conversion between session status and
+     * the API status code is also defined here.
+     */
+    private static final class VendorCallbackWrapper {
+        private final IVibrationSessionCallback mCallback;
+        private final Handler mHandler;
+
+        private boolean mIsStarted;
+        private boolean mIsFinishing;
+        private boolean mIsFinished;
+
+        VendorCallbackWrapper(@NonNull IVibrationSessionCallback callback,
+                @NonNull Handler handler) {
+            mCallback = callback;
+            mHandler = handler;
+        }
+
+        synchronized IBinder getBinderToken() {
+            return mCallback.asBinder();
+        }
+
+        synchronized boolean linkToDeath(DeathRecipient recipient) {
+            try {
+                mCallback.asBinder().linkToDeath(recipient, 0);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error linking session to token death", e);
+                return false;
+            }
+            return true;
+        }
+
+        synchronized void unlinkToDeath(DeathRecipient recipient) {
+            try {
+                mCallback.asBinder().unlinkToDeath(recipient, 0);
+            } catch (NoSuchElementException e) {
+                Slog.wtf(TAG, "Failed to unlink session to token death", e);
+            }
+        }
+
+        synchronized void notifyStarted(IVibrationSession session) {
+            if (mIsStarted) {
+                return;
+            }
+            mIsStarted = true;
+            mHandler.post(() -> {
+                try {
+                    mCallback.onStarted(session);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error notifying vendor session started", e);
+                }
+            });
+        }
+
+        synchronized void notifyFinishing() {
+            if (!mIsStarted || mIsFinishing || mIsFinished) {
+                // Ignore if never started or if already finishing or finished.
+                return;
+            }
+            mIsFinishing = true;
+            mHandler.post(() -> {
+                try {
+                    mCallback.onFinishing();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error notifying vendor session is finishing", e);
+                }
+            });
+        }
+
+        synchronized void notifyFinished(Status status) {
+            if (mIsFinished) {
+                return;
+            }
+            mIsFinished = true;
+            mHandler.post(() -> {
+                try {
+                    mCallback.onFinished(toSessionStatus(status));
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error notifying vendor session finished", e);
+                }
+            });
+        }
+
+        @android.os.vibrator.VendorVibrationSession.Status
+        private static int toSessionStatus(Status status) {
+            // Exhaustive switch to cover all possible internal status.
+            return switch (status) {
+                case FINISHED
+                        -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
+                case IGNORED_UNSUPPORTED
+                        -> STATUS_UNSUPPORTED;
+                case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
+                     CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
+                     CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
+                        -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
+                case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
+                     IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
+                     IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
+                     IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
+                        -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
+                case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING,
+                     IGNORED_ERROR_SCHEDULING, IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES,
+                     FINISHED_UNEXPECTED, RUNNING
+                        -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
+            };
+        }
     }
 
     /**
@@ -499,7 +584,7 @@
         @Override
         public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
             if (mStartTime > 0) {
-                // Only log sessions that have started.
+                // Only log sessions that have started in the HAL.
                 statsLogger.logVibrationVendorSessionStarted(mCallerInfo.uid);
                 statsLogger.logVibrationVendorSessionVibrations(mCallerInfo.uid,
                         mVibrations.size());
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index dcc3cf7..57b82c3 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -344,7 +344,6 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.KeepForWeakReference;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.ResolverActivity;
 import com.android.internal.content.ReferrerIntent;
@@ -854,8 +853,6 @@
 
     private RemoteCallbackList<IScreenCaptureObserver> mCaptureCallbacks;
 
-    // Ensure the field is kept during optimization to preserve downstream weak refs.
-    @KeepForWeakReference
     private final ColorDisplayService.ColorTransformController mColorTransformController =
             (matrix, translation) -> mWmService.mH.post(() -> {
                 synchronized (mWmService.mGlobalLock) {
@@ -2329,13 +2326,16 @@
             if (isActivityTypeHome()) {
                 // The snapshot of home is only used once because it won't be updated while screen
                 // is on (see {@link TaskSnapshotController#screenTurningOff}).
-                mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
                 final Transition transition = mTransitionController.getCollectingTransition();
                 if (transition != null && (transition.getFlags()
                         & WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0) {
+                    mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
                     // Only use snapshot of home as starting window when unlocking directly.
                     return false;
                 }
+                // Add a reference before removing snapshot from cache.
+                snapshot.addReference(TaskSnapshot.REFERENCE_WRITE_TO_PARCEL);
+                mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
             }
             return createSnapshot(snapshot, typeParameter);
         }
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 119709e..f51e60c 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -279,16 +279,24 @@
         mSupervisor = supervisor;
     }
 
+    private ActivityTaskManagerService getService() {
+        return mService;
+    }
+
+    private ActivityTaskSupervisor getSupervisor() {
+        return mSupervisor;
+    }
+
     private boolean isHomeApp(int uid, @Nullable String packageName) {
-        if (mService.mHomeProcess != null) {
+        if (getService().mHomeProcess != null) {
             // Fast check
-            return uid == mService.mHomeProcess.mUid;
+            return uid == getService().mHomeProcess.mUid;
         }
         if (packageName == null) {
             return false;
         }
         ComponentName activity =
-                mService.getPackageManagerInternalLocked()
+                getService().getPackageManagerInternalLocked()
                         .getDefaultHomeActivity(UserHandle.getUserId(uid));
         return activity != null && packageName.equals(activity.getPackageName());
     }
@@ -342,7 +350,8 @@
             mAllowBalExemptionForSystemProcess = allowBalExemptionForSystemProcess;
             mOriginatingPendingIntent = originatingPendingIntent;
             mIntent = intent;
-            mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+            mRealCallingPackage = getService().getPackageNameIfUnique(realCallingUid,
+                    realCallingPid);
             mIsCallForResult = resultRecord != null;
             mCheckedOptions = checkedOptions;
             @BackgroundActivityStartMode int callerBackgroundActivityStartMode =
@@ -401,13 +410,13 @@
                                 checkedOptions, realCallingUid, mRealCallingPackage);
             }
 
-            mAppSwitchState = mService.getBalAppSwitchesState();
-            mCallingUidProcState = mService.mActiveUids.getUidState(callingUid);
+            mAppSwitchState = getService().getBalAppSwitchesState();
+            mCallingUidProcState = getService().mActiveUids.getUidState(callingUid);
             mIsCallingUidPersistentSystemProcess =
                     mCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
             mCallingUidHasVisibleActivity =
-                    mService.mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
-            mCallingUidHasNonAppVisibleWindow = mService.mActiveUids.hasNonAppVisibleWindow(
+                    getService().mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
+            mCallingUidHasNonAppVisibleWindow = getService().mActiveUids.hasNonAppVisibleWindow(
                     callingUid);
             if (realCallingUid == NO_PROCESS_UID) {
                 // no process provided
@@ -422,16 +431,17 @@
                 mRealCallingUidHasNonAppVisibleWindow = mCallingUidHasNonAppVisibleWindow;
                 // In the PendingIntent case callerApp is not passed in, so resolve it ourselves.
                 mRealCallerApp = callerApp == null
-                        ? mService.getProcessController(realCallingPid, realCallingUid)
+                        ? getService().getProcessController(realCallingPid, realCallingUid)
                         : callerApp;
                 mIsRealCallingUidPersistentSystemProcess = mIsCallingUidPersistentSystemProcess;
             } else {
-                mRealCallingUidProcState = mService.mActiveUids.getUidState(realCallingUid);
+                mRealCallingUidProcState = getService().mActiveUids.getUidState(realCallingUid);
                 mRealCallingUidHasVisibleActivity =
-                        mService.mVisibleActivityProcessTracker.hasVisibleActivity(realCallingUid);
+                        getService().mVisibleActivityProcessTracker.hasVisibleActivity(
+                                realCallingUid);
                 mRealCallingUidHasNonAppVisibleWindow =
-                        mService.mActiveUids.hasNonAppVisibleWindow(realCallingUid);
-                mRealCallerApp = mService.getProcessController(realCallingPid, realCallingUid);
+                        getService().mActiveUids.hasNonAppVisibleWindow(realCallingUid);
+                mRealCallerApp = getService().getProcessController(realCallingPid, realCallingUid);
                 mIsRealCallingUidPersistentSystemProcess =
                         mRealCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
             }
@@ -481,7 +491,7 @@
             if (uid == 0) {
                 return "root[debugOnly]";
             }
-            String name = mService.getPackageManagerInternalLocked().getNameForUid(uid);
+            String name = getService().getPackageManagerInternalLocked().getNameForUid(uid);
             if (name == null) {
                 name = "uid=" + uid;
             }
@@ -783,7 +793,7 @@
                     Process.getAppUidForSdkSandboxUid(state.mRealCallingUid);
             // realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition
             // to realCallingUid when calculating resultForRealCaller below.
-            if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
+            if (getService().hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
                 state.setResultForRealCaller(new BalVerdict(BAL_ALLOW_SDK_SANDBOX,
                         /*background*/ false,
                         "uid in SDK sandbox has visible (non-toast) window"));
@@ -1000,37 +1010,45 @@
      * or {@link #BAL_BLOCK} if the launch should be blocked
      */
     BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
+        boolean evaluateVisibleOnly = balAdditionalStartModes()
+                && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
+        if (evaluateVisibleOnly) {
+            return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
+                    mCheckCallerProcessAllowsForeground);
+        }
         if (state.isPendingIntent()) {
             // PendingIntents should mostly be allowed by the sender (real caller) or a permission
             // the creator of the PendingIntent has. Visibility should be the exceptional case, so
             // test it last (this does not change the result, just the bal code).
-            BalVerdict result = BalVerdict.BLOCK;
-            if (!(balAdditionalStartModes()
-                    && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
-                    == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
-                result = checkBackgroundActivityStartAllowedByCallerInBackground(state);
-            }
-            if (result == BalVerdict.BLOCK) {
-                result = checkBackgroundActivityStartAllowedByCallerInForeground(state);
-
-            }
-            return result;
-        } else {
-            BalVerdict result = checkBackgroundActivityStartAllowedByCallerInForeground(state);
-            if (result == BalVerdict.BLOCK && !(balAdditionalStartModes()
-                    && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
-                    == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
-                result = checkBackgroundActivityStartAllowedByCallerInBackground(state);
-            }
-            return result;
+            return evaluateChain(state, mCheckCallerIsAllowlistedUid,
+                    mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
+                    mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
+                    mCheckCallerProcessAllowsBackground, mCheckCallerVisible,
+                    mCheckCallerNonAppVisible, mCheckCallerProcessAllowsForeground);
         }
+        return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
+                mCheckCallerProcessAllowsForeground, mCheckCallerIsAllowlistedUid,
+                mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
+                mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
+                mCheckCallerProcessAllowsBackground);
     }
 
-    /**
-     * @return A code denoting which BAL rule allows an activity to be started,
-     * or {@link #BAL_BLOCK} if the launch should be blocked
-     */
-    BalVerdict checkBackgroundActivityStartAllowedByCallerInForeground(BalState state) {
+    interface BalExemptionCheck {
+        BalVerdict evaluate(BalState state);
+    }
+
+    private BalVerdict evaluateChain(BalState state, BalExemptionCheck... checks) {
+        for (BalExemptionCheck check : checks) {
+            BalVerdict verdict = check.evaluate(state);
+            if (verdict != BalVerdict.BLOCK) {
+                return verdict;
+            }
+        }
+        return BalVerdict.BLOCK;
+    }
+
+    private final BalExemptionCheck mCheckCallerVisible = state -> {
         // This is used to block background activity launch even if the app is still
         // visible to user after user clicking home button.
 
@@ -1044,21 +1062,19 @@
             return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
                     /*background*/ false, "callingUid has visible window");
         }
+        return BalVerdict.BLOCK;
+    };
+
+    private final BalExemptionCheck mCheckCallerNonAppVisible = state -> {
         if (state.mCallingUidHasNonAppVisibleWindow) {
             return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
                     /*background*/ false, "callingUid has non-app visible window "
-                    + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid));
+                    + getService().mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid));
         }
-        // Don't abort if the callerApp or other processes of that uid are considered to be in the
-        // foreground.
-        return checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_FOREGROUND);
-    }
+        return BalVerdict.BLOCK;
+    };
 
-    /**
-     * @return A code denoting which BAL rule allows an activity to be started,
-     * or {@link #BAL_BLOCK} if the launch should be blocked
-     */
-    BalVerdict checkBackgroundActivityStartAllowedByCallerInBackground(BalState state) {
+    private final BalExemptionCheck mCheckCallerIsAllowlistedUid = state -> {
         // don't abort for the most important UIDs
         final int callingAppId = UserHandle.getAppId(state.mCallingUid);
         if (state.mCallingUid == Process.ROOT_UID
@@ -1066,9 +1082,12 @@
                 || callingAppId == Process.NFC_UID) {
             return new BalVerdict(
                     BAL_ALLOW_ALLOWLISTED_UID, /*background*/ false,
-                     "Important callingUid");
+                    "Important callingUid");
         }
+        return BalVerdict.BLOCK;
+    };
 
+    private final BalExemptionCheck mCheckCallerIsAllowlistedComponent = state -> {
         // Always allow home application to start activities.
         if (isHomeApp(state.mCallingUid, state.mCallingPackage)) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
@@ -1076,9 +1095,10 @@
                     "Home app");
         }
 
+        final int callingAppId = UserHandle.getAppId(state.mCallingUid);
         // IME should always be allowed to start activity, like IME settings.
         final WindowState imeWindow =
-                mService.mRootWindowContainer.getCurrentInputMethodWindow();
+                getService().mRootWindowContainer.getCurrentInputMethodWindow();
         if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                     /*background*/ false,
@@ -1091,71 +1111,88 @@
                     /*background*/ false, "callingUid is persistent system process");
         }
 
+        // don't abort if the caller has the same uid as the recents component
+        if (getSupervisor().mRecentTasks.isCallerRecents(state.mCallingUid)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ true, "Recents Component");
+        }
+        // don't abort if the callingUid is the device owner
+        if (getService().isDeviceOwner(state.mCallingUid)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ true, "Device Owner");
+        }
+        // don't abort if the callingUid is a affiliated profile owner
+        if (getService().isAffiliatedProfileOwner(state.mCallingUid)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ true, "Affiliated Profile Owner");
+        }
+        // don't abort if the callingUid has companion device
+        final int callingUserId = UserHandle.getUserId(state.mCallingUid);
+        if (getService().isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ true, "Companion App");
+        }
+        return BalVerdict.BLOCK;
+    };
+
+    private final BalExemptionCheck mCheckCallerHasBackgroundPermission = state -> {
         // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
         if (hasBalPermission(state.mCallingUid, state.mCallingPid)) {
             return new BalVerdict(BAL_ALLOW_PERMISSION,
                     /*background*/ true,
                     "START_ACTIVITIES_FROM_BACKGROUND permission granted");
         }
-        // don't abort if the caller has the same uid as the recents component
-        if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) {
-            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                    /*background*/ true, "Recents Component");
-        }
-        // don't abort if the callingUid is the device owner
-        if (mService.isDeviceOwner(state.mCallingUid)) {
-            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                    /*background*/ true, "Device Owner");
-        }
-        // don't abort if the callingUid is a affiliated profile owner
-        if (mService.isAffiliatedProfileOwner(state.mCallingUid)) {
-            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                    /*background*/ true, "Affiliated Profile Owner");
-        }
-        // don't abort if the callingUid has companion device
-        final int callingUserId = UserHandle.getUserId(state.mCallingUid);
-        if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
-            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                    /*background*/ true, "Companion App");
-        }
+        return BalVerdict.BLOCK;
+    };
+    private final BalExemptionCheck mCheckCallerHasSawPermission = state -> {
         // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
-        if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
+        if (getService().hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
                 state.mCallingPackage)) {
             return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
                     /*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
         }
+        return BalVerdict.BLOCK;
+    };
+    private final BalExemptionCheck mCheckCallerHasBgStartAppOp = state -> {
         // don't abort if the callingUid and callingPackage have the
         // OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
-        if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
+        if (isSystemExemptFlagEnabled() && getService().getAppOpsManager().checkOpNoThrow(
                 AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
                 state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) {
             return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
                     "OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted");
         }
+        return BalVerdict.BLOCK;
+    };
 
-        // Don't abort if the callerApp or other processes of that uid are allowed in any way.
-        return checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_BACKGROUND);
-    }
+
+    // Don't abort if the callerApp or other processes of that uid are considered to be in the
+    // foreground.
+    private final BalExemptionCheck mCheckCallerProcessAllowsForeground =
+            state -> checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_FOREGROUND);
+    // Don't abort if the callerApp or other processes of that uid are allowed in any way.
+    private final BalExemptionCheck mCheckCallerProcessAllowsBackground =
+            state -> checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_BACKGROUND);
 
     /**
      * @return A code denoting which BAL rule allows an activity to be started,
      * or {@link #BAL_BLOCK} if the launch should be blocked
      */
     BalVerdict checkBackgroundActivityStartAllowedByRealCaller(BalState state) {
-        BalVerdict result = checkBackgroundActivityStartAllowedByRealCallerInForeground(state);
-        if (result == BalVerdict.BLOCK && !(balAdditionalStartModes()
+        boolean evaluateVisibleOnly = balAdditionalStartModes()
                 && state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
-                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
-            result = checkBackgroundActivityStartAllowedByRealCallerInBackground(state);
+                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
+        if (evaluateVisibleOnly) {
+            return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
+                    mCheckRealCallerProcessAllowsBalForeground);
         }
-        return result;
+        return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
+                mCheckRealCallerProcessAllowsBalForeground, mCheckRealCallerBalPermission,
+                mCheckRealCallerSawPermission, mCheckRealCallerAllowlistedUid,
+                mCheckRealCallerAllowlistedComponent, mCheckRealCallerProcessAllowsBalBackground);
     }
 
-    /**
-     * @return A code denoting which BAL rule allows an activity to be started,
-     * or {@link #BAL_BLOCK} if the launch should be blocked
-     */
-    BalVerdict checkBackgroundActivityStartAllowedByRealCallerInForeground(BalState state) {
+    private final BalExemptionCheck mCheckRealCallerVisible = state -> {
         // Normal apps with visible app window will be allowed to start activity if app switching
         // is allowed, or apps like live wallpaper with non app visible window will be allowed.
         // The home app can start apps even if app switches are usually disallowed.
@@ -1166,22 +1203,29 @@
             return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
                     /*background*/ false, "realCallingUid has visible window");
         }
+        return BalVerdict.BLOCK;
+    };
+
+    private final BalExemptionCheck mCheckRealCallerNonAppVisible = state -> {
         if (state.mRealCallingUidHasNonAppVisibleWindow) {
             return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
                     /*background*/ false, "realCallingUid has non-app visible window "
-                    + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mRealCallingUid));
+                    + getService().mActiveUids.getNonAppVisibleWindowDetails(
+                    state.mRealCallingUid));
         }
+        return BalVerdict.BLOCK;
+    };
 
-        // Don't abort if the realCallerApp or other processes of that uid are considered to be in
-        // the foreground.
-        return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);
-    }
+    // Don't abort if the realCallerApp or other processes of that uid are considered to be in
+    // the foreground.
+    private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground =
+            state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);
 
-    /**
-     * @return A code denoting which BAL rule allows an activity to be started,
-     * or {@link #BAL_BLOCK} if the launch should be blocked
-     */
-    BalVerdict checkBackgroundActivityStartAllowedByRealCallerInBackground(BalState state) {
+    // don't abort if the callerApp or other processes of that uid are allowed in any way
+    private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground =
+            state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
+
+    private final BalExemptionCheck mCheckRealCallerBalPermission = state -> {
         boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                 == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
         if (allowAlways
@@ -1190,15 +1234,25 @@
                     /*background*/ false,
                     "realCallingUid has BAL permission.");
         }
+        return BalVerdict.BLOCK;
+    };
 
+    private final BalExemptionCheck mCheckRealCallerSawPermission = state -> {
+        boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
+                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
         // don't abort if the realCallingUid has SYSTEM_ALERT_WINDOW permission
         if (allowAlways
-                && mService.hasSystemAlertWindowPermission(state.mRealCallingUid,
+                && getService().hasSystemAlertWindowPermission(state.mRealCallingUid,
                 state.mRealCallingPid, state.mRealCallingPackage)) {
             return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
                     /*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
         }
+        return BalVerdict.BLOCK;
+    };
 
+    private final BalExemptionCheck mCheckRealCallerAllowlistedUid = state -> {
+        boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
+                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
         // if the realCallingUid is a persistent system process, abort if the IntentSender
         // wasn't allowed to start an activity
         if ((allowAlways || state.mAllowBalExemptionForSystemProcess)
@@ -1208,17 +1262,19 @@
                     "realCallingUid is persistent system process AND intent "
                             + "sender forced to allow.");
         }
+        return BalVerdict.BLOCK;
+    };
+
+    private final BalExemptionCheck mCheckRealCallerAllowlistedComponent = state -> {
         // don't abort if the realCallingUid is an associated companion app
-        if (mService.isAssociatedCompanionApp(
+        if (getService().isAssociatedCompanionApp(
                 UserHandle.getUserId(state.mRealCallingUid), state.mRealCallingUid)) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                     /*background*/ false,
                     "realCallingUid is a companion app.");
         }
-
-        // don't abort if the callerApp or other processes of that uid are allowed in any way
-        return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
-    }
+        return BalVerdict.BLOCK;
+    };
 
     @VisibleForTesting boolean hasBalPermission(int uid, int pid) {
         return ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND,
@@ -1245,7 +1301,7 @@
         } else {
             // only if that one wasn't allowed, check the other ones
             final ArraySet<WindowProcessController> uidProcesses =
-                    mService.mProcessMap.getProcesses(app.mUid);
+                    getService().mProcessMap.getProcesses(app.mUid);
             if (uidProcesses != null) {
                 for (int i = uidProcesses.size() - 1; i >= 0; i--) {
                     final WindowProcessController proc = uidProcesses.valueAt(i);
@@ -1416,7 +1472,7 @@
         if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) {
             String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK
                     + (enforceBlock ? " blocked " : " would block ")
-                    + getApplicationLabel(mService.mContext.getPackageManager(),
+                    + getApplicationLabel(getService().mContext.getPackageManager(),
                     launchedFromPackageName);
             showToast(toastText);
 
@@ -1438,7 +1494,7 @@
     }
 
     @VisibleForTesting void showToast(String toastText) {
-        UiThread.getHandler().post(() -> Toast.makeText(mService.mContext,
+        UiThread.getHandler().post(() -> Toast.makeText(getService().mContext,
                 toastText, Toast.LENGTH_LONG).show());
     }
 
@@ -1515,7 +1571,7 @@
             return;
         }
 
-        String packageName =  mService.mContext.getPackageManager().getNameForUid(callingUid);
+        String packageName =  getService().mContext.getPackageManager().getNameForUid(callingUid);
         BalState state = new BalState(callingUid, callingPid, packageName, INVALID_UID,
                 INVALID_PID, null, null, false, null, null, ActivityOptions.makeBasic());
         @BalCode int balCode = checkBackgroundActivityStartAllowedByCaller(state).mCode;
@@ -1576,7 +1632,7 @@
         boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags
                 .shouldRestrictActivitySwitch(callingUid) && bas.mTopActivityOptedIn;
 
-        PackageManager pm = mService.mContext.getPackageManager();
+        PackageManager pm = getService().mContext.getPackageManager();
         String callingPackage = pm.getNameForUid(callingUid);
         final CharSequence callingLabel;
         if (callingPackage == null) {
@@ -1737,7 +1793,7 @@
             return bas.optedIn(ar);
         }
 
-        PackageManager pm = mService.mContext.getPackageManager();
+        PackageManager pm = getService().mContext.getPackageManager();
         ApplicationInfo applicationInfo;
 
         final int sourceUserId = UserHandle.getUserId(sourceUid);
@@ -1794,7 +1850,7 @@
 
         if (sourceRecord == null) {
             joiner.add(prefix + "Source Package: " + targetRecord.launchedFromPackage);
-            String realCallingPackage = mService.mContext.getPackageManager().getNameForUid(
+            String realCallingPackage = getService().mContext.getPackageManager().getNameForUid(
                     realCallingUid);
             joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage);
         } else {
@@ -1829,7 +1885,7 @@
         joiner.add(prefix + "BalCode: " + balCodeToString(balCode));
         joiner.add(prefix + "Allowed By Grace Period: " + allowedByGracePeriod);
         joiner.add(prefix + "LastResumedActivity: "
-                       + recordToString.apply(mService.mLastResumedActivity));
+                       + recordToString.apply(getService().mLastResumedActivity));
         joiner.add(prefix + "System opted into enforcement: " + asmOptSystemIntoEnforcement());
 
         if (mTopFinishedActivity != null) {
@@ -1902,7 +1958,7 @@
     }
 
     private BalVerdict statsLog(BalVerdict finalVerdict, BalState state) {
-        if (finalVerdict.blocks() && mService.isActivityStartsLoggingEnabled()) {
+        if (finalVerdict.blocks() && getService().isActivityStartsLoggingEnabled()) {
             // log aborted activity start to TRON
             mSupervisor
                     .getActivityMetricsLogger()
@@ -2138,7 +2194,7 @@
             return -1;
         }
         try {
-            PackageManager pm = mService.mContext.getPackageManager();
+            PackageManager pm = getService().mContext.getPackageManager();
             return pm.getTargetSdkVersion(packageName);
         } catch (Exception e) {
             return -1;
@@ -2159,8 +2215,8 @@
             this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1;
             this.mDebugInfo = getDebugStringForActivityRecord(ar);
 
-            mService.mH.postDelayed(() -> {
-                synchronized (mService.mGlobalLock) {
+            getService().mH.postDelayed(() -> {
+                synchronized (getService().mGlobalLock) {
                     if (mTaskIdToFinishedActivity.get(taskId) == this) {
                         mTaskIdToFinishedActivity.remove(taskId);
                     }
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index c2255d8..dc42b32 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -79,7 +79,7 @@
     }
 
     @VisibleForTesting
-    static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
+    public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
         if (!shouldEnforceDeviceRestrictions()) {
             return true;
         }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a874ef6..50f12c3 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -157,6 +157,7 @@
 import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
 import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
 import static com.android.window.flags.Flags.enablePersistingDensityScaleForConnectedDisplays;
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -3835,13 +3836,18 @@
 
     /**
      * Looking for the focused window on this display if the top focused display hasn't been
-     * found yet (topFocusedDisplayId is INVALID_DISPLAY) or per-display focused was allowed.
+     * found yet (topFocusedDisplayId is INVALID_DISPLAY), per-display focused was allowed, or
+     * the display is presenting. The last one is needed to update system bar visibility in response
+     * to presentation visibility because per-display focus is needed to change system bar
+     * visibility, but the display shouldn't get global focus when a presentation gets shown.
      *
      * @param topFocusedDisplayId Id of the top focused display.
      * @return The focused window or null if there isn't any or no need to seek.
      */
     WindowState findFocusedWindowIfNeeded(int topFocusedDisplayId) {
-        return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY)
+        return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY
+                || (enablePresentationForConnectedDisplays()
+                && mWmService.mPresentationController.isPresentationVisible(mDisplayId)))
                     ? findFocusedWindow() : null;
     }
 
@@ -6932,6 +6938,8 @@
         /** The actual requested visible inset types for this display */
         private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
 
+        private @InsetsType int mAnimatingTypes = 0;
+
         /** The component name of the top focused window on this display */
         private ComponentName mTopFocusedComponentName = null;
 
@@ -7069,6 +7077,18 @@
             }
             return 0;
         }
+
+        @Override
+        public @InsetsType int getAnimatingTypes() {
+            return mAnimatingTypes;
+        }
+
+        @Override
+        public void setAnimatingTypes(@InsetsType int animatingTypes) {
+            if (mAnimatingTypes != animatingTypes) {
+                mAnimatingTypes = animatingTypes;
+            }
+        }
     }
 
     MagnificationSpec getMagnificationSpec() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 4908df0..ec5b503f 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2564,7 +2564,7 @@
             final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
             // TODO(b/277290737): Move this to the client side, instead of using a proxy.
             callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(getDisplayId(),
-                        rootDisplayAreaId, isImmersiveMode));
+                        rootDisplayAreaId, isImmersiveMode, win.getWindowType()));
         }
 
         // Show transient bars for panic if needed.
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index cee4967..6462a37 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -97,6 +97,20 @@
             @NonNull ImeTracker.Token statsToken) {
     }
 
+    /**
+     * @return {@link WindowInsets.Type.InsetsType}s which are currently animating (showing or
+     * hiding).
+     */
+    default @InsetsType int getAnimatingTypes() {
+        return 0;
+    }
+
+    /**
+     * @param animatingTypes the {@link InsetsType}s, that are currently animating
+     */
+    default void setAnimatingTypes(@InsetsType int animatingTypes) {
+    }
+
     /** Returns {@code target.getWindow()}, or null if {@code target} is {@code null}. */
     static WindowState asWindowOrNull(InsetsControlTarget target) {
         return target != null ? target.getWindow() : null;
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 009d482..2872214 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -790,8 +790,6 @@
         private final Handler mHandler;
         private final String mName;
 
-        private boolean mInsetsAnimationRunning;
-
         Host(Handler handler, String name) {
             mHandler = handler;
             mName = name;
@@ -901,10 +899,5 @@
         public IBinder getWindowToken() {
             return null;
         }
-
-        @Override
-        public void notifyAnimationRunningStateChanged(boolean running) {
-            mInsetsAnimationRunning = running;
-        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 164abab..5e0395f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -225,13 +225,16 @@
         for (int i = mProviders.size() - 1; i >= 0; i--) {
             final InsetsSourceProvider provider = mProviders.valueAt(i);
             final @InsetsType int type = provider.getSource().getType();
+            final boolean isImeProvider = type == WindowInsets.Type.ime();
             if ((type & changedTypes) != 0) {
-                final boolean isImeProvider = type == WindowInsets.Type.ime();
                 changed |= provider.updateClientVisibility(
-                                caller, isImeProvider ? statsToken : null)
+                        caller, isImeProvider ? statsToken : null)
                         // Fake control target cannot change the client visibility, but it should
                         // change the insets with its newly requested visibility.
                         || (caller == provider.getFakeControlTarget());
+            } else if (isImeProvider && android.view.inputmethod.Flags.refactorInsetsController()) {
+                ImeTracker.forLogging().onCancelled(statsToken,
+                        ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY);
             }
         }
         if (changed) {
diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java
index b3cff9c..acc658b 100644
--- a/services/core/java/com/android/server/wm/PresentationController.java
+++ b/services/core/java/com/android/server/wm/PresentationController.java
@@ -16,10 +16,17 @@
 
 package com.android.server.wm;
 
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
+
+import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR;
 import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
 
 import android.annotation.NonNull;
-import android.util.IntArray;
+import android.annotation.Nullable;
+import android.hardware.display.DisplayManager;
+import android.util.SparseArray;
+import android.view.WindowManager.LayoutParams.WindowType;
 
 import com.android.internal.protolog.ProtoLog;
 import com.android.internal.protolog.WmProtoLogGroups;
@@ -27,15 +34,125 @@
 /**
  * Manages presentation windows.
  */
-class PresentationController {
+class PresentationController implements DisplayManager.DisplayListener {
 
-    // TODO(b/395475549): Add support for display add/remove, and activity move across displays.
-    private final IntArray mPresentingDisplayIds = new IntArray();
+    private static class Presentation {
+        @NonNull final WindowState mWin;
+        @NonNull final WindowContainerListener mPresentationListener;
+        // This is the task which started this presentation. This shouldn't be null in most cases
+        // because the intended usage of the Presentation API is that an activity that started a
+        // presentation should control the UI and lifecycle of the presentation window.
+        // However, the API doesn't necessarily requires a host activity to exist (e.g. a background
+        // service can launch a presentation), so this can be null.
+        @Nullable final Task mHostTask;
+        @Nullable final WindowContainerListener mHostTaskListener;
 
-    PresentationController() {}
+        Presentation(@NonNull WindowState win,
+                @NonNull WindowContainerListener presentationListener,
+                @Nullable Task hostTask,
+                @Nullable WindowContainerListener hostTaskListener) {
+            mWin = win;
+            mPresentationListener = presentationListener;
+            mHostTask = hostTask;
+            mHostTaskListener = hostTaskListener;
+        }
 
-    private boolean isPresenting(int displayId) {
-        return mPresentingDisplayIds.contains(displayId);
+        @Override
+        public String toString() {
+            return "{win: " + mWin.getName() + ", display: " + mWin.getDisplayId()
+                    + ", hostTask: " + (mHostTask != null ? mHostTask.getName() : null) + "}";
+        }
+    }
+
+    private final SparseArray<Presentation> mPresentations = new SparseArray();
+
+    @Nullable
+    private Presentation getPresentation(@Nullable WindowState win) {
+        if (win == null) return null;
+        for (int i = 0; i < mPresentations.size(); i++) {
+            final Presentation presentation = mPresentations.valueAt(i);
+            if (win == presentation.mWin) return presentation;
+        }
+        return null;
+    }
+
+    private boolean hasPresentationWindow(int displayId) {
+        return mPresentations.contains(displayId);
+    }
+
+    boolean isPresentationVisible(int displayId) {
+        final Presentation presentation = mPresentations.get(displayId);
+        return presentation != null && presentation.mWin.mToken.isVisibleRequested();
+    }
+
+    boolean canPresent(@NonNull WindowState win, @NonNull DisplayContent displayContent) {
+        return canPresent(win, displayContent, win.mAttrs.type, win.getUid());
+    }
+
+    /**
+     * Checks if a presentation window can be shown on the given display.
+     * If the given |win| is empty, a new presentation window is being created.
+     * If the given |win| is not empty, the window already exists as presentation, and we're
+     * revalidate if the |win| is still qualified to be shown.
+     */
+    boolean canPresent(@Nullable WindowState win, @NonNull DisplayContent displayContent,
+            @WindowType int type, int uid) {
+        if (type == TYPE_PRIVATE_PRESENTATION) {
+            // Private presentations can only be created on private displays.
+            return displayContent.isPrivate();
+        }
+
+        if (type != TYPE_PRESENTATION) {
+            return false;
+        }
+
+        if (!enablePresentationForConnectedDisplays()) {
+            return displayContent.getDisplay().isPublicPresentation();
+        }
+
+        boolean allDisplaysArePresenting = true;
+        for (int i = 0; i < displayContent.mWmService.mRoot.mChildren.size(); i++) {
+            final DisplayContent dc = displayContent.mWmService.mRoot.mChildren.get(i);
+            if (displayContent.mDisplayId != dc.mDisplayId
+                    && !mPresentations.contains(dc.mDisplayId)) {
+                allDisplaysArePresenting = false;
+                break;
+            }
+        }
+        if (allDisplaysArePresenting) {
+            // All displays can't present simultaneously.
+            return false;
+        }
+
+        final int displayId = displayContent.mDisplayId;
+        if (hasPresentationWindow(displayId)
+                && win != null && win != mPresentations.get(displayId).mWin) {
+            // A display can't have multiple presentations.
+            return false;
+        }
+
+        Task hostTask = null;
+        final Presentation presentation = getPresentation(win);
+        if (presentation != null) {
+            hostTask = presentation.mHostTask;
+        } else if (win == null) {
+            final Task globallyFocusedTask =
+                    displayContent.mWmService.mRoot.getTopDisplayFocusedRootTask();
+            if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
+                hostTask = globallyFocusedTask;
+            }
+        }
+        if (hostTask != null && displayId == hostTask.getDisplayId()) {
+            // A presentation can't cover its own host task.
+            return false;
+        }
+        if (hostTask == null && !displayContent.getDisplay().isPublicPresentation()) {
+            // A globally focused host task on a different display is needed to show a
+            // presentation on a non-presenting display.
+            return false;
+        }
+
+        return true;
     }
 
     boolean shouldOccludeActivities(int displayId) {
@@ -45,32 +162,87 @@
         // be shown on them.
         // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
         // the presentation won't stop its controlling activity.
-        return enablePresentationForConnectedDisplays() && isPresenting(displayId);
+        return enablePresentationForConnectedDisplays() && isPresentationVisible(displayId);
     }
 
-    void onPresentationAdded(@NonNull WindowState win) {
+    void onPresentationAdded(@NonNull WindowState win, int uid) {
         final int displayId = win.getDisplayId();
-        if (isPresenting(displayId)) {
-            return;
-        }
         ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s",
-                win.getDisplayId(), win);
-        mPresentingDisplayIds.add(win.getDisplayId());
+                displayId, win);
         win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true);
+
+        final WindowContainerListener presentationWindowListener = new WindowContainerListener() {
+            @Override
+            public void onRemoved() {
+                if (!hasPresentationWindow(displayId)) {
+                    ProtoLog.e(WM_ERROR, "Failed to remove presentation on"
+                            + "non-presenting display %d: %s", displayId, win);
+                    return;
+                }
+                final Presentation presentation = mPresentations.get(displayId);
+                win.mToken.unregisterWindowContainerListener(presentation.mPresentationListener);
+                if (presentation.mHostTask != null) {
+                    presentation.mHostTask.unregisterWindowContainerListener(
+                            presentation.mHostTaskListener);
+                }
+                mPresentations.remove(displayId);
+                win.mWmService.mDisplayManagerInternal.onPresentation(displayId, false /*isShown*/);
+            }
+        };
+        win.mToken.registerWindowContainerListener(presentationWindowListener);
+
+        Task hostTask = null;
+        if (enablePresentationForConnectedDisplays()) {
+            final Task globallyFocusedTask =
+                    win.mWmService.mRoot.getTopDisplayFocusedRootTask();
+            if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
+                hostTask = globallyFocusedTask;
+            }
+        }
+        WindowContainerListener hostTaskListener = null;
+        if (hostTask != null) {
+            hostTaskListener = new WindowContainerListener() {
+                public void onDisplayChanged(DisplayContent dc) {
+                    final Presentation presentation = mPresentations.get(dc.getDisplayId());
+                    if (presentation != null && !canPresent(presentation.mWin, dc)) {
+                        removePresentation(dc.mDisplayId, "host task moved to display "
+                                + dc.getDisplayId());
+                    }
+                }
+
+                public void onRemoved() {
+                    removePresentation(win.getDisplayId(), "host task removed");
+                }
+            };
+            hostTask.registerWindowContainerListener(hostTaskListener);
+        }
+
+        mPresentations.put(displayId, new Presentation(win, presentationWindowListener, hostTask,
+                hostTaskListener));
     }
 
-    void onPresentationRemoved(@NonNull WindowState win) {
-        final int displayId = win.getDisplayId();
-        if (!isPresenting(displayId)) {
-            return;
+    void removePresentation(int displayId, @NonNull String reason) {
+        final Presentation presentation = mPresentations.get(displayId);
+        if (enablePresentationForConnectedDisplays() && presentation != null) {
+            ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Removing Presentation %s for "
+                    + "reason %s", mPresentations.get(displayId), reason);
+            final WindowState win = presentation.mWin;
+            win.mWmService.mAtmService.mH.post(() -> {
+                synchronized (win.mWmService.mGlobalLock) {
+                    win.removeIfPossible();
+                }
+            });
         }
-        ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION,
-                "Presentation removed from display %d: %s", win.getDisplayId(), win);
-        // TODO(b/393945496): Make sure that there's one presentation at most per display.
-        final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId);
-        if (displayIdIndex != -1) {
-            mPresentingDisplayIds.remove(displayIdIndex);
-        }
-        win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false);
     }
+
+    @Override
+    public void onDisplayAdded(int displayId) {}
+
+    @Override
+    public void onDisplayRemoved(int displayId) {
+        removePresentation(displayId, "display removed " + displayId);
+    }
+
+    @Override
+    public void onDisplayChanged(int displayId) {}
 }
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 8d198b2..3ed16db 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -737,6 +737,17 @@
         }
     }
 
+    @Override
+    public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) {
+        synchronized (mService.mGlobalLock) {
+            final WindowState win = mService.windowForClientLocked(this, window,
+                    false /* throwOnError */);
+            if (win != null) {
+                win.setAnimatingTypes(animatingTypes);
+            }
+        }
+    }
+
     void onWindowAdded(WindowState w) {
         if (mPackageName == null) {
             mPackageName = mProcess.mInfo.packageName;
@@ -1015,15 +1026,4 @@
             }
         }
     }
-
-    @Override
-    public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) {
-        synchronized (mService.mGlobalLock) {
-            final WindowState win = mService.windowForClientLocked(this, window,
-                    false /* throwOnError */);
-            if (win != null) {
-                win.notifyInsetsAnimationRunningStateChanged(running);
-            }
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 821c040..28f2825 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1583,14 +1583,18 @@
                 return WindowManagerGlobal.ADD_DUPLICATE_ADD;
             }
 
-            if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
+            if (type == TYPE_PRIVATE_PRESENTATION
+                    && !mPresentationController.canPresent(null /*win*/, displayContent, type,
+                    callingUid)) {
                 ProtoLog.w(WM_ERROR,
                         "Attempted to add private presentation window to a non-private display.  "
                                 + "Aborting.");
                 return WindowManagerGlobal.ADD_PERMISSION_DENIED;
             }
 
-            if (type == TYPE_PRESENTATION && !displayContent.getDisplay().isPublicPresentation()) {
+            if (type == TYPE_PRESENTATION
+                    && !mPresentationController.canPresent(null /*win*/, displayContent, type,
+                    callingUid)) {
                 ProtoLog.w(WM_ERROR,
                         "Attempted to add presentation window to a non-suitable display.  "
                                 + "Aborting.");
@@ -1830,7 +1834,8 @@
                 }
                 win.mTransitionController.collect(win.mToken);
                 res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
-                        outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
+                        outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
+                        callingUid);
                 // A presentation hides all activities behind on the same display.
                 win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
                         /*notifyClients=*/ true);
@@ -1841,7 +1846,8 @@
                 }
             } else {
                 res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
-                        outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
+                        outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
+                        callingUid);
             }
         }
 
@@ -1854,7 +1860,7 @@
             @NonNull ActivityRecord activity, @NonNull DisplayContent displayContent,
             @NonNull InsetsState outInsetsState, @NonNull Rect outAttachedFrame,
             @NonNull InsetsSourceControl.Array outActiveControls, @NonNull IWindow client,
-            @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs) {
+            @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs, int uid) {
         int res = 0;
         final int type = attrs.type;
         boolean imMayMove = true;
@@ -1971,7 +1977,7 @@
         outSizeCompatScale[0] = win.getCompatScaleForClient();
 
         if (res >= ADD_OKAY && win.isPresentation()) {
-            mPresentationController.onPresentationAdded(win);
+            mPresentationController.onPresentationAdded(win, uid);
         }
 
         return res;
@@ -4767,6 +4773,26 @@
         }
     }
 
+    @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
+    @Override
+    public void updateDisplayWindowAnimatingTypes(int displayId, @InsetsType int animatingTypes) {
+        updateDisplayWindowAnimatingTypes_enforcePermission();
+        if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+            final long origId = Binder.clearCallingIdentity();
+            try {
+                synchronized (mGlobalLock) {
+                    final DisplayContent dc = mRoot.getDisplayContent(displayId);
+                    if (dc == null || dc.mRemoteInsetsControlTarget == null) {
+                        return;
+                    }
+                    dc.mRemoteInsetsControlTarget.setAnimatingTypes(animatingTypes);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(origId);
+            }
+        }
+    }
+
     @Override
     public int watchRotation(IRotationWatcher watcher, int displayId) {
         final DisplayContent displayContent;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index ec67dd87..3b7d312 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -736,6 +736,8 @@
 
     private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
 
+    private @InsetsType int mAnimatingTypes = 0;
+
     /**
      * Freeze the insets state in some cases that not necessarily keeps up-to-date to the client.
      * (e.g app exiting transition)
@@ -842,6 +844,27 @@
                 mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
     }
 
+    @Override
+    public @InsetsType int getAnimatingTypes() {
+        return mAnimatingTypes;
+    }
+
+    @Override
+    public void setAnimatingTypes(@InsetsType int animatingTypes) {
+        if (mAnimatingTypes != animatingTypes) {
+            if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+                Trace.instant(TRACE_TAG_WINDOW_MANAGER,
+                        TextUtils.formatSimple("%s: setAnimatingTypes(%s)",
+                                getName(),
+                                animatingTypes));
+            }
+            mInsetsAnimationRunning = animatingTypes != 0;
+            mWmService.scheduleAnimationLocked();
+
+            mAnimatingTypes = animatingTypes;
+        }
+    }
+
     /**
      * Set a freeze state for the window to ignore dispatching its insets state to the client.
      *
@@ -2435,7 +2458,6 @@
                 mAnimatingExit = true;
                 mRemoveOnExit = true;
                 mToken.setVisibleRequested(false);
-                mWmService.mPresentationController.onPresentationRemoved(this);
                 // A presentation hides all activities behind on the same display.
                 mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
                         /*notifyClients=*/ true);
@@ -2656,7 +2678,7 @@
             // The client gave us a touchable region and so first
             // we calculate the untouchable region, then punch that out of our
             // expanded modal region.
-            mTmpRegion.set(0, 0, frame.right, frame.bottom);
+            mTmpRegion.set(0, 0, frame.width(), frame.height());
             mTmpRegion.op(mGivenTouchableRegion, Region.Op.DIFFERENCE);
             region.op(mTmpRegion, Region.Op.DIFFERENCE);
         }
@@ -6079,17 +6101,6 @@
         mWmService.scheduleAnimationLocked();
     }
 
-    void notifyInsetsAnimationRunningStateChanged(boolean running) {
-        if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
-            Trace.instant(TRACE_TAG_WINDOW_MANAGER,
-                    TextUtils.formatSimple("%s: notifyInsetsAnimationRunningStateChanged(%s)",
-                    getName(),
-                    Boolean.toString(running)));
-        }
-        mInsetsAnimationRunning = running;
-        mWmService.scheduleAnimationLocked();
-    }
-
     boolean isInsetsAnimationRunning() {
         return mInsetsAnimationRunning;
     }
diff --git a/services/core/jni/BroadcastRadio/OWNERS b/services/core/jni/BroadcastRadio/OWNERS
index ea4421e..a993823 100644
--- a/services/core/jni/BroadcastRadio/OWNERS
+++ b/services/core/jni/BroadcastRadio/OWNERS
@@ -1,2 +1 @@
 twasilczyk@google.com
-randolphs@google.com
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index e158310..860b6fb 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1814,7 +1814,7 @@
                 t.traceEnd();
             }
 
-            if (!isWatch && !isTv && !isAutomotive
+            if (!isWatch && !isTv && !isAutomotive && !isDesktop
                     && android.security.Flags.aapmApi()) {
                 t.traceBegin("StartAdvancedProtectionService");
                 mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index 5eb23a2..1286648 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -16,29 +16,43 @@
 
 package com.android.server.am;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 
 import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.BackgroundStartPrivileges;
+import android.app.BroadcastOptions;
+import android.app.SystemServiceRegistry;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.TestLooperManager;
 import android.os.UserHandle;
+import android.permission.IPermissionManager;
+import android.permission.PermissionManager;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -47,7 +61,6 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.server.AlarmManagerInternal;
@@ -55,6 +68,7 @@
 import com.android.server.LocalServices;
 import com.android.server.appop.AppOpsService;
 import com.android.server.compat.PlatformCompat;
+import com.android.server.firewall.IntentFirewall;
 import com.android.server.wm.ActivityTaskManagerService;
 
 import org.junit.Rule;
@@ -63,8 +77,11 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
 
 public abstract class BaseBroadcastQueueTest {
 
@@ -97,6 +114,8 @@
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
             .spyStatic(FrameworkStatsLog.class)
             .spyStatic(ProcessList.class)
+            .spyStatic(SystemServiceRegistry.class)
+            .mockStatic(AppGlobals.class)
             .build();
 
 
@@ -119,6 +138,16 @@
     ProcessList mProcessList;
     @Mock
     PlatformCompat mPlatformCompat;
+    @Mock
+    IntentFirewall mIntentFirewall;
+    @Mock
+    IPackageManager mIPackageManager;
+    @Mock
+    AppOpsManager mAppOpsManager;
+    @Mock
+    IPermissionManager mIPermissionManager;
+    @Mock
+    PermissionManager mPermissionManager;
 
     @Mock
     AppStartInfoTracker mAppStartInfoTracker;
@@ -167,22 +196,22 @@
             return getUidForPackage(invocation.getArgument(0));
         }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
 
+        final Context spyContext = spy(mContext);
+        doReturn(mPermissionManager).when(spyContext).getSystemService(PermissionManager.class);
         final ActivityManagerService realAms = new ActivityManagerService(
-                new TestInjector(mContext), mServiceThreadRule.getThread());
+                new TestInjector(spyContext), mServiceThreadRule.getThread());
         realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
         realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
         realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
         realAms.mOomAdjuster.mCachedAppOptimizer = mock(CachedAppOptimizer.class);
         realAms.mOomAdjuster = spy(realAms.mOomAdjuster);
-        ExtendedMockito.doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt()));
+        doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt()));
         realAms.mPackageManagerInt = mPackageManagerInt;
         realAms.mUsageStatsService = mUsageStatsManagerInt;
         realAms.mProcessesReady = true;
         mAms = spy(realAms);
 
-        mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
-        doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
-        doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
+        mSkipPolicy = createBroadcastSkipPolicy();
 
         doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker();
 
@@ -198,6 +227,14 @@
         }
     }
 
+    public BroadcastSkipPolicy createBroadcastSkipPolicy() {
+        final BroadcastSkipPolicy skipPolicy = spy(new BroadcastSkipPolicy(mAms));
+        doReturn(null).when(skipPolicy).shouldSkipAtEnqueueMessage(any(), any());
+        doReturn(null).when(skipPolicy).shouldSkipMessage(any(), any());
+        doReturn(false).when(skipPolicy).disallowBackgroundStart(any());
+        return skipPolicy;
+    }
+
     static int getUidForPackage(@NonNull String packageName) {
         switch (packageName) {
             case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID;
@@ -240,6 +277,11 @@
         public BroadcastQueue getBroadcastQueue(ActivityManagerService service) {
             return null;
         }
+
+        @Override
+        public IntentFirewall getIntentFirewall() {
+            return mIntentFirewall;
+        }
     }
 
     abstract String getTag();
@@ -281,24 +323,35 @@
         ri.activityInfo.packageName = packageName;
         ri.activityInfo.processName = processName;
         ri.activityInfo.name = name;
+        ri.activityInfo.exported = true;
         ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId);
         return ri;
     }
 
+    // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
+    @SuppressWarnings("GuardedBy")
+    ProcessRecord makeProcessRecord(ApplicationInfo info) {
+        final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
+        r.setPid(mNextPid.incrementAndGet());
+        ProcessRecord.updateProcessRecordNodes(r);
+        return r;
+    }
+
     BroadcastFilter makeRegisteredReceiver(ProcessRecord app) {
         return makeRegisteredReceiver(app, 0);
     }
 
     BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
         final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid());
-        return makeRegisteredReceiver(receiverList, priority);
+        return makeRegisteredReceiver(receiverList, priority, null);
     }
 
-    static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority) {
+    static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority,
+            String requiredPermission) {
         final IntentFilter filter = new IntentFilter();
         filter.setPriority(priority);
         final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
-                receiverList.app.info.packageName, null, null, null, receiverList.uid,
+                receiverList.app.info.packageName, null, null, requiredPermission, receiverList.uid,
                 receiverList.userId, false, false, true, receiverList.app.info,
                 mock(PlatformCompat.class));
         receiverList.add(res);
@@ -313,4 +366,62 @@
     ArgumentMatcher<ApplicationInfo> appInfoEquals(int uid) {
         return test -> (test.uid == uid);
     }
+
+    static final class BroadcastRecordBuilder {
+        private BroadcastQueue mQueue = mock(BroadcastQueue.class);
+        private Intent mIntent = mock(Intent.class);
+        private ProcessRecord mProcessRecord = mock(ProcessRecord.class);
+        private String mCallerPackage;
+        private String mCallerFeatureId;
+        private int mCallingPid;
+        private int mCallingUid;
+        private boolean mCallerInstantApp;
+        private String mResolvedType;
+        private String[] mRequiredPermissions;
+        private String[] mExcludedPermissions;
+        private String[] mExcludedPackages;
+        private int mAppOp;
+        private BroadcastOptions mOptions = BroadcastOptions.makeBasic();
+        private List mReceivers = Collections.emptyList();
+        private ProcessRecord mResultToApp;
+        private IIntentReceiver mResultTo;
+        private int mResultCode = Activity.RESULT_OK;
+        private String mResultData;
+        private Bundle mResultExtras;
+        private boolean mSerialized;
+        private boolean mSticky;
+        private boolean mInitialSticky;
+        private int mUserId = UserHandle.USER_SYSTEM;
+        private BackgroundStartPrivileges mBackgroundStartPrivileges =
+                BackgroundStartPrivileges.NONE;
+        private boolean mTimeoutExempt;
+        private BiFunction<Integer, Bundle, Bundle> mFilterExtrasForReceiver;
+        private int mCallerAppProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
+        private PlatformCompat mPlatformCompat = mock(PlatformCompat.class);
+
+        public BroadcastRecordBuilder setIntent(Intent intent) {
+            mIntent = intent;
+            return this;
+        }
+
+        public BroadcastRecordBuilder setRequiredPermissions(String[] requiredPermissions) {
+            mRequiredPermissions = requiredPermissions;
+            return this;
+        }
+
+        public BroadcastRecordBuilder setAppOp(int appOp) {
+            mAppOp = appOp;
+            return this;
+        }
+
+        public BroadcastRecord build() {
+            return new BroadcastRecord(mQueue, mIntent, mProcessRecord, mCallerPackage,
+                    mCallerFeatureId, mCallingPid, mCallingUid, mCallerInstantApp, mResolvedType,
+                    mRequiredPermissions, mExcludedPermissions, mExcludedPackages, mAppOp,
+                    mOptions, mReceivers, mResultToApp, mResultTo, mResultCode, mResultData,
+                    mResultExtras, mSerialized, mSticky, mInitialSticky, mUserId,
+                    mBackgroundStartPrivileges, mTimeoutExempt, mFilterExtrasForReceiver,
+                    mCallerAppProcState, mPlatformCompat);
+        }
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
index 409706b..b32ce49 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
@@ -1803,6 +1803,46 @@
         assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
     }
 
+    @SuppressWarnings("GuardedBy")
+    @DisableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
+    @Test
+    public void testSkipPolicy_atEnqueueTime_flagDisabled() throws Exception {
+        final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT);
+        final Object greenReceiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+        final Object redReceiver = makeManifestReceiver(PACKAGE_RED, CLASS_RED);
+
+        final BroadcastRecord userPresentRecord = makeBroadcastRecord(userPresent,
+                List.of(greenReceiver, redReceiver));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick,
+                List.of(greenReceiver, redReceiver));
+
+        doAnswer(invocation -> {
+            final BroadcastRecord r = invocation.getArgument(0);
+            final Object o = invocation.getArgument(1);
+            if (userPresent.getAction().equals(r.intent.getAction())
+                    && isReceiverEquals(o, greenReceiver)) {
+                return "receiver skipped by test";
+            }
+            return null;
+        }).when(mSkipPolicy).shouldSkipMessage(any(BroadcastRecord.class), any());
+
+        mImpl.enqueueBroadcastLocked(userPresentRecord);
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        // There should be only one broadcast for green process as the other would have
+        // been skipped.
+        verifyPendingRecords(greenQueue, List.of(timeTick));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
     @Test
     public void testSkipPolicy_atEnqueueTime() throws Exception {
         final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT);
@@ -1824,7 +1864,7 @@
                 return "receiver skipped by test";
             }
             return null;
-        }).when(mSkipPolicy).shouldSkipMessage(any(BroadcastRecord.class), any());
+        }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any());
 
         mImpl.enqueueBroadcastLocked(userPresentRecord);
         mImpl.enqueueBroadcastLocked(timeTickRecord);
@@ -2270,19 +2310,11 @@
         assertFalse(mImpl.isProcessFreezable(greenProcess));
     }
 
-    // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
-    private ProcessRecord makeProcessRecord(ApplicationInfo info) {
-        final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
-        r.setPid(mNextPid.incrementAndGet());
-        ProcessRecord.updateProcessRecordNodes(r);
-        return r;
-    }
-
     BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
         final IIntentReceiver receiver = mock(IIntentReceiver.class);
         final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
                 UserHandle.getUserId(app.info.uid), receiver);
-        return makeRegisteredReceiver(receiverList, priority);
+        return makeRegisteredReceiver(receiverList, priority, null /* requiredPermission */);
     }
 
     private Intent createPackageChangedIntent(int uid, List<String> componentNameList) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index ad35b25..3a9c99d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -2301,6 +2301,52 @@
     }
 
     /**
+     * Verify that we skip broadcasts at enqueue if {@link BroadcastSkipPolicy} decides it
+     * should be skipped.
+     */
+    @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
+    @Test
+    public void testSkipPolicy_atEnqueueTime() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        final Object greenReceiver = makeRegisteredReceiver(receiverGreenApp);
+        final Object blueReceiver = makeRegisteredReceiver(receiverBlueApp);
+        final Object yellowReceiver = makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW);
+        final Object orangeReceiver = makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE);
+
+        doAnswer(invocation -> {
+            final BroadcastRecord r = invocation.getArgument(0);
+            final Object o = invocation.getArgument(1);
+            if (airplane.getAction().equals(r.intent.getAction())
+                    && (isReceiverEquals(o, greenReceiver)
+                    || isReceiverEquals(o, orangeReceiver))) {
+                return "test skipped receiver";
+            }
+            return null;
+        }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any());
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(greenReceiver, blueReceiver, yellowReceiver, orangeReceiver)));
+
+        waitForIdle();
+        // Verify that only blue and yellow receiver apps received the broadcast.
+        verifyScheduleRegisteredReceiver(never(), receiverGreenApp, USER_SYSTEM);
+        verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class),
+                eq(greenReceiver));
+        verifyScheduleRegisteredReceiver(receiverBlueApp, airplane);
+        final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+                getUidForPackage(PACKAGE_YELLOW));
+        verifyScheduleReceiver(receiverYellowApp, airplane);
+        final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+                getUidForPackage(PACKAGE_ORANGE));
+        assertNull(receiverOrangeApp);
+        verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class),
+                eq(orangeReceiver));
+    }
+
+    /**
      * Verify broadcasts to runtime receivers in cached processes are deferred
      * until that process leaves the cached state.
      */
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java
new file mode 100644
index 0000000..c8aad79e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.am;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+public class BroadcastSkipPolicyTest extends BaseBroadcastQueueTest {
+    private static final String TAG = "BroadcastSkipPolicyTest";
+
+    BroadcastSkipPolicy mBroadcastSkipPolicy;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mBroadcastSkipPolicy = new BroadcastSkipPolicy(mAms);
+
+        doReturn(true).when(mIntentFirewall).checkBroadcast(any(Intent.class),
+                anyInt(), anyInt(), nullable(String.class), anyInt());
+
+        doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
+        doReturn(true).when(mIPackageManager).isPackageAvailable(anyString(), anyInt());
+
+        doReturn(ActivityManager.APP_START_MODE_NORMAL).when(mAms).getAppStartModeLOSP(anyInt(),
+                anyString(), anyInt(), anyInt(), eq(true), eq(false), eq(false));
+
+        doReturn(mAppOpsManager).when(mAms).getAppOpsManager();
+        doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).checkOpNoThrow(anyString(),
+                anyInt(), anyString(), nullable(String.class));
+        doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).noteOpNoThrow(anyString(),
+                anyInt(), anyString(), nullable(String.class), anyString());
+
+        doReturn(mIPermissionManager).when(AppGlobals::getPermissionManager);
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mIPermissionManager).checkUidPermission(
+                anyInt(), anyString(), anyInt());
+    }
+
+    @Override
+    public String getTag() {
+        return TAG;
+    }
+
+    @Override
+    public BroadcastSkipPolicy createBroadcastSkipPolicy() {
+        return new BroadcastSkipPolicy(mAms);
+    }
+
+    @Test
+    public void testShouldSkipMessage_withManifestRcvr_withCompPerm_invokesNoteOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .build();
+        final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+                makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN,
+                        Manifest.permission.PACKAGE_USAGE_STATS));
+        assertNull(msg);
+        verify(mAppOpsManager).noteOpNoThrow(
+                eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+                eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId),
+                anyString());
+        verify(mAppOpsManager, never()).checkOpNoThrow(
+                anyString(), anyInt(), anyString(), nullable(String.class));
+    }
+
+    @Test
+    public void testShouldSkipMessage_withRegRcvr_withCompPerm_invokesNoteOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .build();
+        final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+                makeRegisteredReceiver(receiverApp, 0 /* priority */,
+                        Manifest.permission.PACKAGE_USAGE_STATS));
+        assertNull(msg);
+        verify(mAppOpsManager).noteOpNoThrow(
+                eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+                eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId),
+                anyString());
+        verify(mAppOpsManager, never()).checkOpNoThrow(
+                anyString(), anyInt(), anyString(), nullable(String.class));
+    }
+
+    @Test
+    public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withCompPerm_invokesCheckOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .build();
+        final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+                makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN,
+                        Manifest.permission.PACKAGE_USAGE_STATS));
+        assertNull(msg);
+        verify(mAppOpsManager).checkOpNoThrow(
+                eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+                eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId));
+        verify(mAppOpsManager, never()).noteOpNoThrow(
+                anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+    }
+
+    @Test
+    public void testShouldSkipAtEnqueueMessage_withRegRcvr_withCompPerm_invokesCheckOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .build();
+        final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+                makeRegisteredReceiver(receiverApp, 0 /* priority */,
+                        Manifest.permission.PACKAGE_USAGE_STATS));
+        assertNull(msg);
+        verify(mAppOpsManager).checkOpNoThrow(
+                eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+                eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId));
+        verify(mAppOpsManager, never()).noteOpNoThrow(
+                anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+    }
+
+    @Test
+    public void testShouldSkipMessage_withManifestRcvr_withAppOp_invokesNoteOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+                .build();
+        final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+        final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, receiver);
+        assertNull(msg);
+        verify(mAppOpsManager).noteOpNoThrow(
+                eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+                eq(receiver.activityInfo.applicationInfo.uid),
+                eq(receiver.activityInfo.packageName), nullable(String.class), anyString());
+        verify(mAppOpsManager, never()).checkOpNoThrow(
+                anyString(), anyInt(), anyString(), nullable(String.class));
+    }
+
+    @Test
+    public void testShouldSkipMessage_withRegRcvr_withAppOp_invokesNoteOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+                .build();
+        final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */,
+                null /* requiredPermission */);
+        final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, filter);
+        assertNull(msg);
+        verify(mAppOpsManager).noteOpNoThrow(
+                eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+                eq(filter.receiverList.uid),
+                eq(filter.packageName), nullable(String.class), anyString());
+        verify(mAppOpsManager, never()).checkOpNoThrow(
+                anyString(), anyInt(), anyString(), nullable(String.class));
+    }
+
+    @Test
+    public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withAppOp_invokesCheckOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+                .build();
+        final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+        final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, receiver);
+        assertNull(msg);
+        verify(mAppOpsManager).checkOpNoThrow(
+                eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+                eq(receiver.activityInfo.applicationInfo.uid),
+                eq(receiver.activityInfo.applicationInfo.packageName), nullable(String.class));
+        verify(mAppOpsManager, never()).noteOpNoThrow(
+                anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+    }
+
+    @Test
+    public void testShouldSkipAtEnqueueMessage_withRegRcvr_withAppOp_invokesCheckOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+                .build();
+        final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */,
+                null /* requiredPermission */);
+        final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, filter);
+        assertNull(msg);
+        verify(mAppOpsManager).checkOpNoThrow(
+                eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+                eq(filter.receiverList.uid),
+                eq(filter.packageName), nullable(String.class));
+        verify(mAppOpsManager, never()).noteOpNoThrow(
+                anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+    }
+
+    @Test
+    public void testShouldSkipMessage_withManifestRcvr_withRequiredPerms_invokesNoteOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+                .build();
+        final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+                makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN));
+        assertNull(msg);
+        verify(mPermissionManager).checkPermissionForDataDelivery(
+                eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+        verify(mPermissionManager, never()).checkPermissionForPreflight(
+                eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+    }
+
+    @Test
+    public void testShouldSkipMessage_withRegRcvr_withRequiredPerms_invokesNoteOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+                .build();
+        final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+                makeRegisteredReceiver(receiverApp, 0 /* priority */,
+                        null /* requiredPermission */));
+        assertNull(msg);
+        verify(mPermissionManager).checkPermissionForDataDelivery(
+                eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+        verify(mPermissionManager, never()).checkPermissionForPreflight(
+                eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+    }
+
+    @Test
+    public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withRequiredPerms_invokesCheckOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+                .build();
+        final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+                makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN));
+        assertNull(msg);
+        verify(mPermissionManager, never()).checkPermissionForDataDelivery(
+                eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+        verify(mPermissionManager).checkPermissionForPreflight(
+                eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+    }
+
+    @Test
+    public void testShouldSkipAtEnqueueMessage_withRegRcvr_withRequiredPerms_invokesCheckOp() {
+        final BroadcastRecord record = new BroadcastRecordBuilder()
+                .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+                .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+                .build();
+        final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+                makeRegisteredReceiver(receiverApp, 0 /* priority */,
+                        null /* requiredPermission */));
+        assertNull(msg);
+        verify(mPermissionManager, never()).checkPermissionForDataDelivery(
+                eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+        verify(mPermissionManager).checkPermissionForPreflight(
+                eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+    }
+
+    private ResolveInfo makeManifestReceiverWithPermission(String packageName, String name,
+            String permission) {
+        final ResolveInfo resolveInfo = makeManifestReceiver(packageName, name);
+        resolveInfo.activityInfo.permission = permission;
+        return resolveInfo;
+    }
+
+    private BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority,
+            String requiredPermission) {
+        final IIntentReceiver receiver = mock(IIntentReceiver.class);
+        final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
+                UserHandle.getUserId(app.info.uid), receiver);
+        return makeRegisteredReceiver(receiverList, priority, requiredPermission);
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 4e4b3df..3e87943 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -2388,6 +2388,104 @@
         }
     }
 
+    @Test
+    @EnableFlags({Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS,
+            Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER})
+    public void testGetTimeUntilQuotaConsumedLocked_Installer() {
+        PackageInfo pi = new PackageInfo();
+        pi.packageName = SOURCE_PACKAGE;
+        pi.requestedPermissions = new String[]{Manifest.permission.INSTALL_PACKAGES};
+        pi.requestedPermissionsFlags = new int[]{PackageInfo.REQUESTED_PERMISSION_GRANTED};
+        pi.applicationInfo = new ApplicationInfo();
+        pi.applicationInfo.uid = mSourceUid;
+        doReturn(List.of(pi)).when(mPackageManager).getInstalledPackagesAsUser(anyInt(), anyInt());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mContext).checkPermission(
+                eq(Manifest.permission.INSTALL_PACKAGES), anyInt(), eq(mSourceUid));
+        mQuotaController.onSystemServicesReady();
+
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        // Close to RARE boundary.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(
+                        now - (mQcConstants.WINDOW_SIZE_RARE_MS - 30 * SECOND_IN_MILLIS),
+                        90 * SECOND_IN_MILLIS, 5), false);
+        // Far away from FREQUENT boundary.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(
+                        now - (mQcConstants.WINDOW_SIZE_FREQUENT_MS -  HOUR_IN_MILLIS),
+                        2 * MINUTE_IN_MILLIS, 5), false);
+        // Overlap WORKING_SET boundary.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(
+                        now - (mQcConstants.WINDOW_SIZE_WORKING_MS + MINUTE_IN_MILLIS),
+                        2 * MINUTE_IN_MILLIS, 5), false);
+        // Close to ACTIVE boundary.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(
+                        now - (mQcConstants.WINDOW_SIZE_ACTIVE_MS -  MINUTE_IN_MILLIS),
+                        2 * MINUTE_IN_MILLIS, 5), false);
+        // Close to EXEMPTED boundary.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(
+                        now - (mQcConstants.WINDOW_SIZE_EXEMPTED_MS -  MINUTE_IN_MILLIS),
+                        2 * MINUTE_IN_MILLIS, 5), false);
+
+        // No additional quota for the system installer when the app is in RARE, FREQUENT,
+        // WORKING_SET or ACTIVE bucket.
+        setStandbyBucket(RARE_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(30 * SECOND_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(2 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+
+        setStandbyBucket(FREQUENT_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(2 * MINUTE_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(2 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+
+        setStandbyBucket(WORKING_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(5 * MINUTE_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(6 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+
+        // ACTIVE window != allowed time.
+        setStandbyBucket(ACTIVE_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(6 * MINUTE_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(8 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+
+        // Additional quota for the system installer when the app is in EXEMPTED bucket.
+        // EXEMPTED window == allowed time.
+        setStandbyBucket(EXEMPTED_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(18 * MINUTE_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 8 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+    }
+
     /**
      * Test getTimeUntilQuotaConsumedLocked when the app is close to the max execution limit.
      */
@@ -4119,6 +4217,12 @@
         assertEquals(85 * SECOND_IN_MILLIS, mQuotaController.getEJRewardNotificationSeenMs());
         assertEquals(84 * SECOND_IN_MILLIS, mQuotaController.getEJGracePeriodTempAllowlistMs());
         assertEquals(83 * SECOND_IN_MILLIS, mQuotaController.getEJGracePeriodTopAppMs());
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+        setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+                6 * MINUTE_IN_MILLIS);
+        assertEquals(6 * MINUTE_IN_MILLIS,
+                mQuotaController.getAllowedTimePeriodAdditionInstallerMs());
     }
 
     @Test
@@ -4222,6 +4326,13 @@
         assertEquals(0, mQuotaController.getEJGracePeriodTempAllowlistMs());
         assertEquals(0, mQuotaController.getEJGracePeriodTopAppMs());
 
+        mSetFlagsRule.enableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+        setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+                -MINUTE_IN_MILLIS);
+        assertEquals(0,
+                mQuotaController.getAllowedTimePeriodAdditionInstallerMs());
+        mSetFlagsRule.disableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+
         // Invalid configurations.
         // In_QUOTA_BUFFER should never be greater than ALLOWED_TIME_PER_PERIOD
         setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS,
@@ -4237,9 +4348,18 @@
                 10 * MINUTE_IN_MILLIS);
         setDeviceConfigLong(QcConstants.KEY_IN_QUOTA_BUFFER_MS, 5 * MINUTE_IN_MILLIS);
 
-        assertTrue(mQuotaController.getInQuotaBufferMs()
+        assertTrue(mQuotaController.getAllowedTimePeriodAdditionInstallerMs()
                 <= mQuotaController.getAllowedTimePerPeriodMs()[FREQUENT_INDEX]);
 
+        mSetFlagsRule.enableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+        // ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER should never be greater than
+        // ALLOWED_TIME_PER_PERIOD.
+        setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+                 15 * MINUTE_IN_MILLIS);
+        assertTrue(mQuotaController.getInQuotaBufferMs()
+                <= mQuotaController.getAllowedTimePerPeriodMs()[EXEMPTED_INDEX]);
+        mSetFlagsRule.disableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+
         // Test larger than a day. Controller should cap at one day.
         setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS,
                 25 * HOUR_IN_MILLIS);
@@ -4318,6 +4438,12 @@
         assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getEJRewardNotificationSeenMs());
         assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJGracePeriodTempAllowlistMs());
         assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJGracePeriodTopAppMs());
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+        setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+                25 * HOUR_IN_MILLIS);
+        assertEquals(0, mQuotaController.getAllowedTimePeriodAdditionInstallerMs());
+        mSetFlagsRule.disableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
     }
 
     /** Tests that TimingSessions aren't saved when the device is charging. */
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index bada337..6b8ef88 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -64,7 +64,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ServiceInfo;
-import android.content.res.Resources;
 import android.graphics.Color;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
@@ -95,6 +94,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
+import com.android.server.wm.DesktopModeHelper;
 import com.android.server.wm.WindowManagerInternal;
 
 import org.hamcrest.CoreMatchers;
@@ -155,8 +155,6 @@
 
     private IPackageManager mIpm = AppGlobals.getPackageManager();
 
-    private Resources mResources = sContext.getResources();
-
     @Mock
     private DisplayManager mDisplayManager;
 
@@ -178,6 +176,7 @@
                 .spyStatic(WallpaperUtils.class)
                 .spyStatic(LocalServices.class)
                 .spyStatic(WallpaperManager.class)
+                .spyStatic(DesktopModeHelper.class)
                 .startMocking();
 
         sWindowManagerInternal = mock(WindowManagerInternal.class);
@@ -246,6 +245,8 @@
             int userId = (invocation.getArgument(0));
             return getWallpaperTestDir(userId);
         }).when(() -> WallpaperUtils.getWallpaperDir(anyInt()));
+        ExtendedMockito.doAnswer(invocation -> true).when(
+                () -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any()));
 
         sContext.addMockSystemService(DisplayManager.class, mDisplayManager);
 
@@ -257,10 +258,6 @@
         doReturn(displays).when(mDisplayManager).getDisplays();
 
         spyOn(mIpm);
-        spyOn(mResources);
-        doReturn(true).when(mResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
-        doReturn(true).when(mResources).getBoolean(
-                eq(R.bool.config_canInternalDisplayHostDesktops));
         mService = new TestWallpaperManagerService(sContext);
         spyOn(mService);
         mService.systemReady();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 563baac..b0ffebb 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -2441,6 +2441,80 @@
         assertFalse(mWasCecDisabledOnStandbyByLowEnergyMode);
     }
 
+    @Test
+    public void sendSystemAudioModeRequest_sendsRequest_retry() throws Exception {
+        // Enable System Audio Control
+        mHdmiControlService.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
+                HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
+        mNativeWrapper.setPortConnectionStatus(1, true);
+
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        HdmiCecMessage reportPowerStatus =
+                HdmiCecMessageBuilder.buildReportPowerStatus(ADDR_AUDIO_SYSTEM, ADDR_TV,
+                        HdmiControlManager.POWER_STATUS_STANDBY);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mNativeWrapper.onCecMessage(reportPowerStatus);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.setMessageSendResult(Constants.MESSAGE_SYSTEM_AUDIO_MODE_REQUEST,
+                SendMessageResult.NACK);
+        mTestLooper.dispatchAll();
+
+        // Use SystemAudioAutoInitiationAction to trigger SystemAudioActionFromTv
+        HdmiCecFeatureAction systemAudioAutoInitiationAction =
+                new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM);
+        mHdmiCecLocalDeviceTv.addAndStartAction(systemAudioAutoInitiationAction);
+        HdmiCecMessage reportSystemAudioMode =
+                HdmiCecMessageBuilder.buildReportSystemAudioMode(
+                        ADDR_AUDIO_SYSTEM,
+                        mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(),
+                        true);
+        mHdmiControlService.handleCecCommand(reportSystemAudioMode);
+        mTestLooper.dispatchAll();
+
+        assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).hasSize(1);
+    }
+
+    @Test
+    public void sendSystemAudioModeRequest_sendsRequest_return() throws Exception {
+        // Enable System Audio Control
+        mHdmiControlService.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
+                HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
+        mNativeWrapper.setPortConnectionStatus(1, true);
+
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        HdmiCecMessage reportPowerStatus =
+                HdmiCecMessageBuilder.buildReportPowerStatus(ADDR_AUDIO_SYSTEM, ADDR_TV,
+                        HdmiControlManager.POWER_STATUS_STANDBY);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mNativeWrapper.onCecMessage(reportPowerStatus);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.setMessageSendResult(Constants.MESSAGE_SYSTEM_AUDIO_MODE_REQUEST,
+                SendMessageResult.FAIL);
+        mTestLooper.dispatchAll();
+
+        // Use SystemAudioAutoInitiationAction to trigger SystemAudioActionFromTv
+        HdmiCecFeatureAction systemAudioAutoInitiationAction =
+                new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM);
+        mHdmiCecLocalDeviceTv.addAndStartAction(systemAudioAutoInitiationAction);
+        HdmiCecMessage reportSystemAudioMode =
+                HdmiCecMessageBuilder.buildReportSystemAudioMode(
+                        ADDR_AUDIO_SYSTEM,
+                        mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(),
+                        true);
+        mHdmiControlService.handleCecCommand(reportSystemAudioMode);
+        mTestLooper.dispatchAll();
+
+        assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).hasSize(0);
+    }
+
     protected static class MockTvDevice extends HdmiCecLocalDeviceTv {
         MockTvDevice(HdmiControlService service) {
             super(service);
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 a4e77c0..1de864c 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
@@ -17,9 +17,9 @@
 package com.android.server.location.contexthub;
 
 import static com.google.common.truth.Truth.assertThat;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -33,15 +33,21 @@
 import android.hardware.contexthub.IContextHubEndpoint;
 import android.hardware.contexthub.IContextHubEndpointCallback;
 import android.hardware.contexthub.IEndpointCommunication;
+import android.hardware.contexthub.Message;
 import android.hardware.contexthub.MessageDeliveryStatus;
 import android.hardware.contexthub.Reason;
+import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoAppState;
 import android.os.Binder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
-
+import android.util.Log;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import java.util.Collections;
+import java.util.List;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -51,11 +57,11 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
-import java.util.Collections;
-
 @RunWith(AndroidJUnit4.class)
 @Presubmit
 public class ContextHubEndpointTest {
+    private static final String TAG = "ContextHubEndpointTest";
+
     private static final int SESSION_ID_RANGE = ContextHubEndpointManager.SERVICE_SESSION_RANGE;
     private static final int MIN_SESSION_ID = 0;
     private static final int MAX_SESSION_ID = MIN_SESSION_ID + SESSION_ID_RANGE - 1;
@@ -206,6 +212,68 @@
         assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
     }
 
+    @Test
+    public void testMessageTransaction() throws RemoteException {
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+        testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ true);
+
+        unregisterExampleEndpoint(endpoint);
+    }
+
+    @Test
+    public void testMessageTransactionCleanupOnUnregistration() throws RemoteException {
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+        testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ false);
+
+        unregisterExampleEndpoint(endpoint);
+        assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0);
+    }
+
+    /** A helper method to create a session and validates reliable message sending. */
+    private void testMessageTransactionInternal(
+            IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException {
+        HubEndpointInfo targetInfo =
+                new HubEndpointInfo(
+                        TARGET_ENDPOINT_NAME,
+                        TARGET_ENDPOINT_ID,
+                        ENDPOINT_PACKAGE_NAME,
+                        Collections.emptyList());
+        int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null);
+        mEndpointManager.onEndpointSessionOpenComplete(sessionId);
+
+        final int messageType = 1234;
+        HubMessage message =
+                new HubMessage.Builder(messageType, new byte[] {1, 2, 3, 4, 5})
+                        .setResponseRequired(true)
+                        .build();
+        IContextHubTransactionCallback callback =
+                new IContextHubTransactionCallback.Stub() {
+                    @Override
+                    public void onQueryResponse(int result, List<NanoAppState> nanoappList) {
+                        Log.i(TAG, "Received onQueryResponse callback, result=" + result);
+                    }
+
+                    @Override
+                    public void onTransactionComplete(int result) {
+                        Log.i(TAG, "Received onTransactionComplete callback, result=" + result);
+                    }
+                };
+        endpoint.sendMessage(sessionId, message, callback);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+        verify(mMockEndpointCommunications, timeout(1000))
+                .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture());
+        Message halMessage = messageCaptor.getValue();
+        assertThat(halMessage.type).isEqualTo(message.getMessageType());
+        assertThat(halMessage.content).isEqualTo(message.getMessageBody());
+        assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(1);
+
+        if (deliverMessageStatus) {
+            mEndpointManager.onMessageDeliveryStatusReceived(
+                    sessionId, halMessage.sequenceNumber, ErrorCode.OK);
+            assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0);
+        }
+    }
+
     private IContextHubEndpoint registerExampleEndpoint() throws RemoteException {
         HubEndpointInfo info =
                 new HubEndpointInfo(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
index b332331..6b989cb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
@@ -38,6 +38,7 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.Condition;
 
+import com.android.internal.R;
 import com.android.server.UiServiceTestCase;
 
 import org.junit.Before;
@@ -46,6 +47,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.List;
+
 public class ConditionProvidersTest extends UiServiceTestCase {
 
     private ConditionProviders mProviders;
@@ -169,4 +172,15 @@
 
         assertTrue(mProviders.getApproved(userId, true).isEmpty());
     }
+
+    @Test
+    public void getDefaultDndAccessPackages_returnsPackages() {
+        mContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultDndAccessPackages,
+                "com.example.a:com.example.b::::com.example.c");
+
+        List<String> packages = ConditionProviders.getDefaultDndAccessPackages(mContext);
+
+        assertThat(packages).containsExactly("com.example.a", "com.example.b", "com.example.c");
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 67e85ff..5dea44d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -4495,27 +4495,6 @@
     }
 
     @Test
-    public void testBubblePreference_sameVersionWithSAWPermission() throws Exception {
-        when(mAppOpsManager.noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(),
-                anyString(), eq(null), anyString())).thenReturn(MODE_ALLOWED);
-
-        final String xml = "<ranking version=\"4\">\n"
-                + "<package name=\"" + PKG_O + "\" uid=\"" + UID_O + "\">\n"
-                + "<channel id=\"someId\" name=\"hi\""
-                + " importance=\"3\"/>"
-                + "</package>"
-                + "</ranking>";
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())),
-                null);
-        parser.nextTag();
-        mHelper.readXml(parser, false, UserHandle.USER_ALL);
-
-        assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O));
-        assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O));
-    }
-
-    @Test
     public void testBubblePreference_upgradeWithSAWThenUserOverride() throws Exception {
         when(mAppOpsManager.noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(),
                 anyString(), eq(null), anyString())).thenReturn(MODE_ALLOWED);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java
new file mode 100644
index 0000000..154a905
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ZenConfigTrimmerTest extends UiServiceTestCase {
+
+    private static final String TRUSTED_PACKAGE = "com.trust.me";
+    private static final int ONE_PERCENT = 1_500;
+
+    private ZenConfigTrimmer mTrimmer;
+
+    @Before
+    public void setUp() {
+        mContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultDndAccessPackages, TRUSTED_PACKAGE);
+
+        mTrimmer = new ZenConfigTrimmer(mContext);
+    }
+
+    @Test
+    public void trimToMaximumSize_belowMax_untouched() {
+        ZenModeConfig config = new ZenModeConfig();
+        addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT);
+        addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+
+        mTrimmer.trimToMaximumSize(config);
+
+        assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "4", "5");
+    }
+
+    @Test
+    public void trimToMaximumSize_exceedsMax_removesAllRulesOfLargestPackages() {
+        ZenModeConfig config = new ZenModeConfig();
+        addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT);
+        addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+        addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT);
+        addZenRule(config, "7", "pkg4", 38 * ONE_PERCENT);
+
+        mTrimmer.trimToMaximumSize(config);
+
+        assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "6");
+        assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct())
+                .containsExactly("pkg1", "pkg3");
+    }
+
+    @Test
+    public void trimToMaximumSize_keepsRulesFromTrustedPackages() {
+        ZenModeConfig config = new ZenModeConfig();
+        addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+        addZenRule(config, "4", TRUSTED_PACKAGE, 60 * ONE_PERCENT);
+        addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+        addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT);
+
+        mTrimmer.trimToMaximumSize(config);
+
+        assertThat(config.automaticRules.keySet()).containsExactly("4", "5");
+        assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct())
+                .containsExactly(TRUSTED_PACKAGE, "pkg2");
+    }
+
+    /**
+     * Create a ZenRule that, when serialized to a Parcel, will take <em>approximately</em>
+     * {@code desiredSize} bytes (within 100 bytes). Try to make the tests not rely on a very tight
+     * fit.
+     */
+    private static void addZenRule(ZenModeConfig config, String id, String pkg, int desiredSize) {
+        ZenRule rule = new ZenRule();
+        rule.id = id;
+        rule.pkg = pkg;
+        config.automaticRules.put(id, rule);
+
+        // Make the ZenRule as large as desired. Not to the exact byte, because otherwise this
+        // test would have to be adjusted whenever we change the parceling of ZenRule in any way.
+        // (Still might need adjustment if we change the serialization _significantly_).
+        int nameLength = desiredSize - id.length() - pkg.length() - 232;
+        rule.name = "A".repeat(nameLength);
+
+        Parcel verification = Parcel.obtain();
+        try {
+            verification.writeParcelable(rule, 0);
+            assertThat(verification.dataSize()).isWithin(100).of(desiredSize);
+        } finally {
+            verification.recycle();
+        }
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index f8387a4..51891ef 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -90,6 +90,7 @@
 import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG;
 import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW;
 import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
+import static com.android.server.notification.Flags.FLAG_LIMIT_ZEN_CONFIG_SIZE;
 import static com.android.server.notification.Flags.FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING;
 import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL;
 import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
@@ -236,6 +237,7 @@
 @SmallTest
 @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
 @RunWith(ParameterizedAndroidJunit4.class)
+@EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE) // Should be parameterization, but off path does nothing.
 @TestableLooper.RunWithLooper
 public class ZenModeHelperTest extends UiServiceTestCase {
 
@@ -7480,6 +7482,45 @@
         assertThat(getZenRule(ruleId).lastActivation).isNull();
     }
 
+    @Test
+    @EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE)
+    public void addAutomaticZenRule_trimsConfiguration() {
+        mZenModeHelper.mConfig.automaticRules.clear();
+        AutomaticZenRule smallRule = new AutomaticZenRule.Builder("Reasonable", CONDITION_ID)
+                .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+                .build();
+        AutomaticZenRule systemRule = new AutomaticZenRule.Builder("System", CONDITION_ID)
+                .setOwner(new ComponentName("android", "ScheduleConditionProvider"))
+                .build();
+
+        AutomaticZenRule bigRule = new AutomaticZenRule.Builder("Yuge", CONDITION_ID)
+                .setConfigurationActivity(new ComponentName("evil.package", "cls"))
+                .setTriggerDescription("0123456789".repeat(6000)) // ~60k bytes utf16.
+                .build();
+
+        String systemRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android",
+                systemRule, ORIGIN_SYSTEM, "add", SYSTEM_UID);
+        String smallRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, smallRule,
+                ORIGIN_APP, "add", CUSTOM_PKG_UID);
+        String bigRuleId1 =  mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+                bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+                systemRuleId, smallRuleId, bigRuleId1);
+
+        String bigRuleId2 =  mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+                bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+                systemRuleId, smallRuleId, bigRuleId1, bigRuleId2);
+
+        // This should go over the threshold
+        String bigRuleId3 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+                bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+
+        // Rules from evil.package are gone.
+        assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+                systemRuleId, smallRuleId);
+    }
+
     private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
             @Nullable ZenPolicy zenPolicy) {
         ZenRule rule = new ZenRule();
diff --git a/services/tests/wmtests/src/com/android/server/TransitionSubject.java b/services/tests/wmtests/src/com/android/server/TransitionSubject.java
new file mode 100644
index 0000000..07026b9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/TransitionSubject.java
@@ -0,0 +1,79 @@
+/*
+ * 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.wm;
+
+import android.annotation.Nullable;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TransitionSubject extends Subject {
+
+    @Nullable
+    private final Transition actual;
+
+    /**
+     * Internal constructor.
+     *
+     * @see TransitionSubject#assertThat(Transition)
+     */
+    private TransitionSubject(FailureMetadata metadata, @Nullable Transition actual) {
+        super(metadata, actual);
+        this.actual = actual;
+    }
+
+    /**
+     * In a fluent assertion chain, the argument to the "custom" overload of {@link
+     * StandardSubjectBuilder#about(CustomSubjectBuilder.Factory) about}, the method that specifies
+     * what kind of {@link Subject} to create.
+     */
+    public static Factory<TransitionSubject, Transition> transitions() {
+        return TransitionSubject::new;
+    }
+
+    /**
+     * Typical entry point for making assertions about Transitions.
+     *
+     * @see @Truth#assertThat(Object)
+     */
+    public static TransitionSubject assertThat(Transition transition) {
+        return Truth.assertAbout(transitions()).that(transition);
+    }
+
+    /**
+     * Converts to a {@link IterableSubject} containing {@link Transition#getFlags()} separated into
+     * a list of individual flags for assertions such as {@code flags().contains(TRANSIT_FLAG_XYZ)}.
+     *
+     * <p>If the subject is null, this will fail instead of returning a null subject.
+     */
+    public IterableSubject flags() {
+        isNotNull();
+
+        final List<Integer> sortedFlags = new ArrayList<>();
+        for (int i = 0; i < 32; i++) {
+            if ((actual.getFlags() & (1 << i)) != 0) {
+                sortedFlags.add((1 << i));
+            }
+        }
+        return com.google.common.truth.Truth.assertThat(sortedFlags);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index cfd501a..61ed0b5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -63,6 +63,9 @@
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
 import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
@@ -80,6 +83,7 @@
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
+import static com.android.server.wm.TransitionSubject.assertThat;
 import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
 import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT;
@@ -147,6 +151,7 @@
 import com.android.server.LocalServices;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.wm.utils.WmDisplayCutout;
+import com.android.window.flags.Flags;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -2620,6 +2625,7 @@
         final KeyguardController keyguard = mAtm.mKeyguardController;
         final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
         final int displayId = mDisplayContent.getDisplayId();
+        final TestTransitionPlayer transitions = registerTestTransitionPlayer();
 
         final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
         final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
@@ -2629,21 +2635,40 @@
         keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
         assertFalse(keyguardGoingAway.getAsBoolean());
         assertFalse(appVisible.getAsBoolean());
+        transitions.flush();
 
         // Start unlocking from AOD.
         keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
         assertTrue(keyguardGoingAway.getAsBoolean());
         assertTrue(appVisible.getAsBoolean());
 
+        if (Flags.ensureKeyguardDoesTransitionStarting()) {
+            assertThat(transitions.mLastTransit).isNull();
+        } else {
+            assertThat(transitions.mLastTransit).flags()
+                    .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY);
+        }
+        transitions.flush();
+
         // Clear AOD. This does *not* clear the going-away status.
         keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
         assertTrue(keyguardGoingAway.getAsBoolean());
         assertTrue(appVisible.getAsBoolean());
 
+        if (Flags.aodTransition()) {
+            assertThat(transitions.mLastTransit).flags()
+                    .containsExactly(TRANSIT_FLAG_AOD_APPEARING);
+        } else {
+            assertThat(transitions.mLastTransit).isNull();
+        }
+        transitions.flush();
+
         // Finish unlock
         keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */);
         assertFalse(keyguardGoingAway.getAsBoolean());
         assertTrue(appVisible.getAsBoolean());
+
+        assertThat(transitions.mLastTransit).isNull();
     }
 
     @Test
@@ -2653,6 +2678,7 @@
         final KeyguardController keyguard = mAtm.mKeyguardController;
         final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
         final int displayId = mDisplayContent.getDisplayId();
+        final TestTransitionPlayer transitions = registerTestTransitionPlayer();
 
         final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
         final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
@@ -2662,22 +2688,44 @@
         keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
         assertFalse(keyguardGoingAway.getAsBoolean());
         assertFalse(appVisible.getAsBoolean());
+        transitions.flush();
 
         // Start unlocking from AOD.
         keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
         assertTrue(keyguardGoingAway.getAsBoolean());
         assertTrue(appVisible.getAsBoolean());
 
+        if (!Flags.ensureKeyguardDoesTransitionStarting()) {
+            assertThat(transitions.mLastTransit).flags()
+                    .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY);
+        }
+        transitions.flush();
+
         // Clear AOD. This does *not* clear the going-away status.
         keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
         assertTrue(keyguardGoingAway.getAsBoolean());
         assertTrue(appVisible.getAsBoolean());
 
+        if (Flags.aodTransition()) {
+            assertThat(transitions.mLastTransit).flags()
+                    .containsExactly(TRANSIT_FLAG_AOD_APPEARING);
+        } else {
+            assertThat(transitions.mLastTransit).isNull();
+        }
+        transitions.flush();
+
         // Same API call a second time cancels the unlock, because AOD isn't changing.
         keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
         assertTrue(keyguardShowing.getAsBoolean());
         assertFalse(keyguardGoingAway.getAsBoolean());
         assertFalse(appVisible.getAsBoolean());
+
+        if (Flags.ensureKeyguardDoesTransitionStarting()) {
+            assertThat(transitions.mLastTransit).isNull();
+        } else {
+            assertThat(transitions.mLastTransit).flags()
+                    .containsExactly(TRANSIT_FLAG_KEYGUARD_APPEARING);
+        }
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
index 2d4101e..6e0f7fb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
@@ -16,9 +16,12 @@
 
 package com.android.server.wm;
 
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.FLAG_PRESENTATION;
+import static android.view.Display.FLAG_TRUSTED;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_WAKE;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
@@ -30,6 +33,7 @@
 
 import android.annotation.NonNull;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.UserHandle;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
@@ -118,6 +122,112 @@
         assertFalse(window.isAttached());
     }
 
+    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+    @Test
+    public void testPresentationCannotCoverHostTask() {
+        int uid = Binder.getCallingUid();
+        final DisplayContent presentationDisplay = createPresentationDisplay();
+        final Task task = createTask(presentationDisplay);
+        task.effectiveUid = uid;
+        final ActivityRecord activity = createActivityRecord(task);
+        assertTrue(activity.isVisible());
+
+        // Adding a presentation window over its host task must fail.
+        assertAddPresentationWindowFails(uid, presentationDisplay.mDisplayId);
+
+        // Adding a presentation window on the other display must succeed.
+        final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+        final Transition addTransition = window.mTransitionController.getCollectingTransition();
+        completeTransition(addTransition, /*abortSync=*/ true);
+        assertTrue(window.isVisible());
+
+        // Moving the host task to the presenting display will remove the presentation.
+        task.reparent(mDefaultDisplay.getDefaultTaskDisplayArea(), true);
+        waitHandlerIdle(window.mWmService.mAtmService.mH);
+        final Transition removeTransition = window.mTransitionController.getCollectingTransition();
+        assertEquals(TRANSIT_CLOSE, removeTransition.mType);
+        completeTransition(removeTransition, /*abortSync=*/ false);
+        assertFalse(window.isVisible());
+    }
+
+    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+    @Test
+    public void testPresentationCannotLaunchOnAllDisplays() {
+        final int uid = Binder.getCallingUid();
+        final DisplayContent presentationDisplay = createPresentationDisplay();
+        final Task task = createTask(presentationDisplay);
+        task.effectiveUid = uid;
+        final ActivityRecord activity = createActivityRecord(task);
+        assertTrue(activity.isVisible());
+
+        // Add a presentation window on the default display.
+        final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+        final Transition addTransition = window.mTransitionController.getCollectingTransition();
+        completeTransition(addTransition, /*abortSync=*/ true);
+        assertTrue(window.isVisible());
+
+        // Adding another presentation window over the task even if it's a different UID because
+        // it would end up showing presentations on all displays.
+        assertAddPresentationWindowFails(uid + 1, presentationDisplay.mDisplayId);
+    }
+
+    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+    @Test
+    public void testPresentationCannotLaunchOnNonPresentationDisplayWithoutHostHavingGlobalFocus() {
+        final int uid = Binder.getCallingUid();
+        // Adding a presentation window on an internal display requires a host task
+        // with global focus on another display.
+        assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);
+
+        final DisplayContent presentationDisplay = createPresentationDisplay();
+        final Task taskWiSameUid = createTask(presentationDisplay);
+        taskWiSameUid.effectiveUid = uid;
+        final ActivityRecord activity = createActivityRecord(taskWiSameUid);
+        assertTrue(activity.isVisible());
+        final Task taskWithDifferentUid = createTask(presentationDisplay);
+        taskWithDifferentUid.effectiveUid = uid + 1;
+        createActivityRecord(taskWithDifferentUid);
+        assertEquals(taskWithDifferentUid, presentationDisplay.getFocusedRootTask());
+
+        // The task with the same UID is covered by another task with a different UID, so this must
+        // also fail.
+        assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);
+
+        // Moving the task with the same UID to front and giving it global focus allows a
+        // presentation to show on the default display.
+        taskWiSameUid.moveToFront("test");
+        final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+        final Transition addTransition = window.mTransitionController.getCollectingTransition();
+        completeTransition(addTransition, /*abortSync=*/ true);
+        assertTrue(window.isVisible());
+    }
+
+    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+    @Test
+    public void testReparentingActivityToSameDisplayClosesPresentation() {
+        final int uid = Binder.getCallingUid();
+        final Task task = createTask(mDefaultDisplay);
+        task.effectiveUid = uid;
+        final ActivityRecord activity = createActivityRecord(task);
+        assertTrue(activity.isVisible());
+
+        // Add a presentation window on a presentation display.
+        final DisplayContent presentationDisplay = createPresentationDisplay();
+        final WindowState window = addPresentationWindow(uid, presentationDisplay.getDisplayId());
+        final Transition addTransition = window.mTransitionController.getCollectingTransition();
+        completeTransition(addTransition, /*abortSync=*/ true);
+        assertTrue(window.isVisible());
+
+        // Reparenting the host task below the presentation must close the presentation.
+        task.reparent(presentationDisplay.getDefaultTaskDisplayArea(), true);
+        waitHandlerIdle(window.mWmService.mAtmService.mH);
+        final Transition removeTransition = window.mTransitionController.getCollectingTransition();
+        // It's a WAKE transition instead of CLOSE because
+        assertEquals(TRANSIT_WAKE, removeTransition.mType);
+        completeTransition(removeTransition, /*abortSync=*/ false);
+        assertFalse(window.isVisible());
+    }
+
     private WindowState addPresentationWindow(int uid, int displayId) {
         final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
         final int userId = UserHandle.getUserId(uid);
@@ -134,10 +244,29 @@
         return window;
     }
 
+    private void assertAddPresentationWindowFails(int uid, int displayId) {
+        final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
+        final IWindow clientWindow = new TestIWindow();
+        final int res = addPresentationWindowInner(uid, displayId, session, clientWindow);
+        assertEquals(WindowManagerGlobal.ADD_INVALID_DISPLAY, res);
+    }
+
+    private int addPresentationWindowInner(int uid, int displayId, Session session,
+            IWindow clientWindow) {
+        final int userId = UserHandle.getUserId(uid);
+        doReturn(true).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
+        final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_PRESENTATION);
+        return mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, userId,
+                WindowInsets.Type.defaultVisible(), null, new InsetsState(),
+                new InsetsSourceControl.Array(), new Rect(), new float[1]);
+    }
+
     private DisplayContent createPresentationDisplay() {
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.copyFrom(mDisplayInfo);
-        displayInfo.flags = FLAG_PRESENTATION;
+        displayInfo.flags = FLAG_PRESENTATION | FLAG_TRUSTED;
+        displayInfo.displayId = DEFAULT_DISPLAY + 1;
         final DisplayContent dc = createNewDisplay(displayInfo);
         final int displayId = dc.getDisplayId();
         doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index 45436e4..d3f3269 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -33,6 +33,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.view.Display.Mode;
 import android.view.Surface;
+import android.view.WindowInsets;
 import android.view.WindowManager.LayoutParams;
 
 import androidx.test.filters.SmallTest;
@@ -283,7 +284,7 @@
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
 
-        overrideWindow.notifyInsetsAnimationRunningStateChanged(true);
+        overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars());
         assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
         assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
         assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
@@ -303,7 +304,7 @@
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
 
-        overrideWindow.notifyInsetsAnimationRunningStateChanged(true);
+        overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars());
         assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
         assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
         assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index c0642f5..57ab13f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -2151,6 +2151,14 @@
             mLastRequest = null;
         }
 
+        void flush() {
+            if (mLastTransit != null) {
+                start();
+                finish();
+                clear();
+            }
+        }
+
         @Override
         public void onTransitionReady(IBinder transitToken, TransitionInfo transitionInfo,
                 SurfaceControl.Transaction transaction, SurfaceControl.Transaction finishT)
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index ca4a643..ae7346e 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -1737,13 +1737,8 @@
     private int getCardIdForDefaultEuicc() {
         int cardId = TelephonyManager.UNINITIALIZED_CARD_ID;
 
-        if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
-            PackageManager pm = mContext.getPackageManager();
-            if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
-                TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
-                cardId = tm.getCardIdForDefaultEuicc();
-            }
-        } else {
+        PackageManager pm = mContext.getPackageManager();
+        if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
             TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
             cardId = tm.getCardIdForDefaultEuicc();
         }
diff --git a/tools/processors/view_inspector/OWNERS b/tools/processors/view_inspector/OWNERS
index 0473f54..38d21e1 100644
--- a/tools/processors/view_inspector/OWNERS
+++ b/tools/processors/view_inspector/OWNERS
@@ -1,3 +1,2 @@
 alanv@google.com
-ashleyrose@google.com
-aurimas@google.com
\ No newline at end of file
+aurimas@google.com