Merge "Address API feedback for cross-user MediaRouter2" into main
diff --git a/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java b/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java
index 3c361d7..95730e8 100644
--- a/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java
+++ b/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java
@@ -122,6 +122,8 @@
             Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true)
                     .recycle();
         }
+        source.recycle();
+        Runtime.getRuntime().gc();
     }
 
     @Test
@@ -141,6 +143,8 @@
             Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true)
                     .recycle();
         }
+        source.recycle();
+        Runtime.getRuntime().gc();
     }
 
     @Test
@@ -158,5 +162,7 @@
             Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true)
                     .recycle();
         }
+        source.recycle();
+        Runtime.getRuntime().gc();
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 44afbe6..14cce19 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -16,6 +16,11 @@
 
 package com.android.server.job.controllers;
 
+import static android.app.job.JobInfo.PRIORITY_DEFAULT;
+import static android.app.job.JobInfo.PRIORITY_HIGH;
+import static android.app.job.JobInfo.PRIORITY_LOW;
+import static android.app.job.JobInfo.PRIORITY_MAX;
+import static android.app.job.JobInfo.PRIORITY_MIN;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
@@ -43,9 +48,12 @@
 import android.provider.DeviceConfig;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
+import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.SparseArrayMap;
+import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.TimeUtils;
 
@@ -91,6 +99,23 @@
      */
     private long mFallbackFlexibilityDeadlineMs =
             FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
+    /**
+     * The default deadline that all flexible constraints should be dropped by if a job lacks
+     * a deadline, keyed by job priority.
+     */
+    private SparseLongArray mFallbackFlexibilityDeadlines =
+            FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES;
+    /**
+     * The scores to use for each job, keyed by job priority.
+     */
+    private SparseIntArray mFallbackFlexibilityDeadlineScores =
+            FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES;
+    /**
+     * The amount of time to add (scaled by job run score) to the fallback flexibility deadline,
+     * keyed by job priority.
+     */
+    private SparseLongArray mFallbackFlexibilityAdditionalScoreTimeFactors =
+            FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS;
 
     private long mRescheduledJobDeadline = FcConfig.DEFAULT_RESCHEDULED_JOB_DEADLINE_MS;
     private long mMaxRescheduledDeadline = FcConfig.DEFAULT_MAX_RESCHEDULED_DEADLINE_MS;
@@ -117,10 +142,10 @@
 
     /**
      * The percent of a job's lifecycle to drop number of required constraints.
-     * mPercentToDropConstraints[i] denotes that at x% of a Jobs lifecycle,
-     * the controller should have i+1 constraints dropped.
+     * mPercentsToDropConstraints[i] denotes that at x% of a Jobs lifecycle,
+     * the controller should have i+1 constraints dropped. Keyed by job priority.
      */
-    private int[] mPercentToDropConstraints;
+    private SparseArray<int[]> mPercentsToDropConstraints;
 
     /**
      * Keeps track of what flexible constraints are satisfied at the moment.
@@ -199,6 +224,86 @@
                 }
             };
 
+    /** Helper object to track job run score for each app. */
+    private static class JobScoreTracker {
+        private static class JobScoreBucket {
+            @ElapsedRealtimeLong
+            public long startTimeElapsed;
+            public int score;
+
+            private void reset() {
+                startTimeElapsed = 0;
+                score = 0;
+            }
+        }
+
+        private static final int NUM_SCORE_BUCKETS = 24;
+        private static final long MAX_TIME_WINDOW_MS = 24 * HOUR_IN_MILLIS;
+        private final JobScoreBucket[] mScoreBuckets = new JobScoreBucket[NUM_SCORE_BUCKETS];
+        private int mScoreBucketIndex = 0;
+
+        public void addScore(int add, long nowElapsed) {
+            JobScoreBucket bucket = mScoreBuckets[mScoreBucketIndex];
+            if (bucket == null) {
+                bucket = new JobScoreBucket();
+                bucket.startTimeElapsed = nowElapsed;
+                mScoreBuckets[mScoreBucketIndex] = bucket;
+            } else if (bucket.startTimeElapsed < nowElapsed - MAX_TIME_WINDOW_MS) {
+                // The bucket is too old.
+                bucket.reset();
+                bucket.startTimeElapsed = nowElapsed;
+            } else if (bucket.startTimeElapsed
+                    < nowElapsed - MAX_TIME_WINDOW_MS / NUM_SCORE_BUCKETS) {
+                // The current bucket's duration has completed. Move on to the next bucket.
+                mScoreBucketIndex = (mScoreBucketIndex + 1) % NUM_SCORE_BUCKETS;
+                addScore(add, nowElapsed);
+                return;
+            }
+
+            bucket.score += add;
+        }
+
+        public int getScore(long nowElapsed) {
+            int score = 0;
+            final long earliestElapsed = nowElapsed - MAX_TIME_WINDOW_MS;
+            for (JobScoreBucket bucket : mScoreBuckets) {
+                if (bucket != null && bucket.startTimeElapsed >= earliestElapsed) {
+                    score += bucket.score;
+                }
+            }
+            return score;
+        }
+
+        public void dump(@NonNull IndentingPrintWriter pw, long nowElapsed) {
+            pw.print("{");
+
+            boolean printed = false;
+            for (int x = 0; x < mScoreBuckets.length; ++x) {
+                final int idx = (mScoreBucketIndex + 1 + x) % mScoreBuckets.length;
+                final JobScoreBucket jsb = mScoreBuckets[idx];
+                if (jsb == null || jsb.startTimeElapsed == 0) {
+                    continue;
+                }
+                if (printed) {
+                    pw.print(", ");
+                }
+                TimeUtils.formatDuration(jsb.startTimeElapsed, nowElapsed, pw);
+                pw.print("=");
+                pw.print(jsb.score);
+                printed = true;
+            }
+
+            pw.print("}");
+        }
+    }
+
+    /**
+     * Set of {@link JobScoreTracker JobScoreTrackers} for each app.
+     * Keyed by source UID -> source package.
+     **/
+    private final SparseArrayMap<String, JobScoreTracker> mJobScoreTrackers =
+            new SparseArrayMap<>();
+
     private static final int MSG_CHECK_ALL_JOBS = 0;
     /** Check the jobs in {@link #mJobsToCheck} */
     private static final int MSG_CHECK_JOBS = 1;
@@ -228,8 +333,8 @@
         mFcConfig = new FcConfig();
         mFlexibilityAlarmQueue = new FlexibilityAlarmQueue(
                 mContext, AppSchedulingModuleThread.get().getLooper());
-        mPercentToDropConstraints =
-                mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+        mPercentsToDropConstraints =
+                FcConfig.DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS;
         mPrefetchController = prefetchController;
 
         if (mFlexibilityEnabled) {
@@ -272,6 +377,36 @@
     }
 
     @Override
+    public void prepareForExecutionLocked(JobStatus jobStatus) {
+        // Use the job's requested priority to determine its score since that is what the developer
+        // selected and it will be stable across job runs.
+        final int score = mFallbackFlexibilityDeadlineScores
+                .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100);
+        JobScoreTracker jobScoreTracker =
+                mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName());
+        if (jobScoreTracker == null) {
+            jobScoreTracker = new JobScoreTracker();
+            mJobScoreTrackers.add(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(),
+                    jobScoreTracker);
+        }
+        jobScoreTracker.addScore(score, sElapsedRealtimeClock.millis());
+    }
+
+    @Override
+    public void unprepareFromExecutionLocked(JobStatus jobStatus) {
+        // The job didn't actually start. Undo the score increase.
+        JobScoreTracker jobScoreTracker =
+                mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName());
+        if (jobScoreTracker == null) {
+            Slog.e(TAG, "Unprepared a job that didn't result in a score change");
+            return;
+        }
+        final int score = mFallbackFlexibilityDeadlineScores
+                .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100);
+        jobScoreTracker.addScore(-score, sElapsedRealtimeClock.millis());
+    }
+
+    @Override
     @GuardedBy("mLock")
     public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob) {
         if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) {
@@ -286,12 +421,33 @@
     public void onAppRemovedLocked(String packageName, int uid) {
         final int userId = UserHandle.getUserId(uid);
         mPrefetchLifeCycleStart.delete(userId, packageName);
+        mJobScoreTrackers.delete(uid, packageName);
+        for (int i = mJobsToCheck.size() - 1; i >= 0; --i) {
+            final JobStatus js = mJobsToCheck.valueAt(i);
+            if ((js.getSourceUid() == uid && js.getSourcePackageName().equals(packageName))
+                    || (js.getUid() == uid && js.getCallingPackageName().equals(packageName))) {
+                mJobsToCheck.removeAt(i);
+            }
+        }
     }
 
     @Override
     @GuardedBy("mLock")
     public void onUserRemovedLocked(int userId) {
         mPrefetchLifeCycleStart.delete(userId);
+        for (int u = mJobScoreTrackers.numMaps() - 1; u >= 0; --u) {
+            final int uid = mJobScoreTrackers.keyAt(u);
+            if (UserHandle.getUserId(uid) == userId) {
+                mJobScoreTrackers.deleteAt(u);
+            }
+        }
+        for (int i = mJobsToCheck.size() - 1; i >= 0; --i) {
+            final JobStatus js = mJobsToCheck.valueAt(i);
+            if (UserHandle.getUserId(js.getSourceUid()) == userId
+                    || UserHandle.getUserId(js.getUid()) == userId) {
+                mJobsToCheck.removeAt(i);
+            }
+        }
     }
 
     boolean isEnabled() {
@@ -308,9 +464,9 @@
                 || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP
                 // Only exclude DEFAULT+ priority jobs for BFGS+ apps
                 || (mService.getUidBias(js.getSourceUid()) >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
-                        && js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT)
+                        && js.getEffectivePriority() >= PRIORITY_DEFAULT)
                 // For apps in the power allowlist, automatically exclude DEFAULT+ priority jobs.
-                || (js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT
+                || (js.getEffectivePriority() >= PRIORITY_DEFAULT
                         && mPowerAllowlistedApps.contains(js.getSourcePackageName()))
                 || hasEnoughSatisfiedConstraintsLocked(js)
                 || mService.isCurrentlyRunningLocked(js);
@@ -462,7 +618,14 @@
 
     @VisibleForTesting
     @GuardedBy("mLock")
-    long getLifeCycleEndElapsedLocked(JobStatus js, long earliest) {
+    int getScoreLocked(int uid, @NonNull String pkgName, long nowElapsed) {
+        final JobScoreTracker scoreTracker = mJobScoreTrackers.get(uid, pkgName);
+        return scoreTracker == null ? 0 : scoreTracker.getScore(nowElapsed);
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    long getLifeCycleEndElapsedLocked(JobStatus js, long nowElapsed, long earliest) {
         if (js.getJob().isPrefetch()) {
             final long estimatedLaunchTime =
                     mPrefetchController.getNextEstimatedLaunchTimeLocked(js);
@@ -486,15 +649,28 @@
                     (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2),
                     mMaxRescheduledDeadline);
         }
-        return js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME
-                ? earliest + mFallbackFlexibilityDeadlineMs : js.getLatestRunTimeElapsed();
+        if (js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME) {
+            // Intentionally use the effective priority here. If a job's priority was effectively
+            // lowered, it will be less likely to run quickly given other policies in JobScheduler.
+            // Thus, there's no need to further delay the job based on flex policy.
+            final int jobPriority = js.getEffectivePriority();
+            final int jobScore =
+                    getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed);
+            // Set an upper limit on the fallback deadline so that the delay doesn't become extreme.
+            final long fallbackDeadlineMs = Math.min(3 * mFallbackFlexibilityDeadlineMs,
+                    mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs)
+                            + mFallbackFlexibilityAdditionalScoreTimeFactors
+                                    .get(jobPriority, MINUTE_IN_MILLIS) * jobScore);
+            return earliest + fallbackDeadlineMs;
+        }
+        return js.getLatestRunTimeElapsed();
     }
 
     @VisibleForTesting
     @GuardedBy("mLock")
     int getCurPercentOfLifecycleLocked(JobStatus js, long nowElapsed) {
         final long earliest = getLifeCycleBeginningElapsedLocked(js);
-        final long latest = getLifeCycleEndElapsedLocked(js, earliest);
+        final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest);
         if (latest == NO_LIFECYCLE_END || earliest >= nowElapsed) {
             return 0;
         }
@@ -510,7 +686,8 @@
     @GuardedBy("mLock")
     long getNextConstraintDropTimeElapsedLocked(JobStatus js) {
         final long earliest = getLifeCycleBeginningElapsedLocked(js);
-        final long latest = getLifeCycleEndElapsedLocked(js, earliest);
+        final long latest =
+                getLifeCycleEndElapsedLocked(js, sElapsedRealtimeClock.millis(), earliest);
         return getNextConstraintDropTimeElapsedLocked(js, earliest, latest);
     }
 
@@ -518,15 +695,27 @@
     @ElapsedRealtimeLong
     @GuardedBy("mLock")
     long getNextConstraintDropTimeElapsedLocked(JobStatus js, long earliest, long latest) {
+        final int[] percentsToDropConstraints =
+                getPercentsToDropConstraints(js.getEffectivePriority());
         if (latest == NO_LIFECYCLE_END
-                || js.getNumDroppedFlexibleConstraints() == mPercentToDropConstraints.length) {
+                || js.getNumDroppedFlexibleConstraints() == percentsToDropConstraints.length) {
             return NO_LIFECYCLE_END;
         }
-        final int percent = mPercentToDropConstraints[js.getNumDroppedFlexibleConstraints()];
+        final int percent = percentsToDropConstraints[js.getNumDroppedFlexibleConstraints()];
         final long percentInTime = ((latest - earliest) * percent) / 100;
         return earliest + percentInTime;
     }
 
+    @NonNull
+    private int[] getPercentsToDropConstraints(int priority) {
+        int[] percentsToDropConstraints = mPercentsToDropConstraints.get(priority);
+        if (percentsToDropConstraints == null) {
+            Slog.wtf(TAG, "No %-to-drop for priority " + JobInfo.getPriorityString(priority));
+            return new int[]{50, 60, 70, 80};
+        }
+        return percentsToDropConstraints;
+    }
+
     @Override
     @GuardedBy("mLock")
     public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
@@ -681,10 +870,12 @@
                     Integer.bitCount(getRelevantAppliedConstraintsLocked(js));
             js.setNumAppliedFlexibleConstraints(numAppliedConstraints);
 
+            final int[] percentsToDropConstraints =
+                    getPercentsToDropConstraints(js.getEffectivePriority());
             final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed);
             int toDrop = 0;
             for (int i = 0; i < numAppliedConstraints; i++) {
-                if (curPercent >= mPercentToDropConstraints[i]) {
+                if (curPercent >= percentsToDropConstraints[i]) {
                     toDrop++;
                 }
             }
@@ -705,8 +896,10 @@
             final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed);
             int toDrop = 0;
             final int jsMaxFlexibleConstraints = js.getNumAppliedFlexibleConstraints();
+            final int[] percentsToDropConstraints =
+                    getPercentsToDropConstraints(js.getEffectivePriority());
             for (int i = 0; i < jsMaxFlexibleConstraints; i++) {
-                if (curPercent >= mPercentToDropConstraints[i]) {
+                if (curPercent >= percentsToDropConstraints[i]) {
                     toDrop++;
                 }
             }
@@ -733,7 +926,7 @@
             return mTrackedJobs.size();
         }
 
-        public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
+        public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate, long nowElapsed) {
             for (int i = 0; i < mTrackedJobs.size(); i++) {
                 ArraySet<JobStatus> jobs = mTrackedJobs.get(i);
                 for (int j = 0; j < jobs.size(); j++) {
@@ -744,8 +937,18 @@
                     js.printUniqueId(pw);
                     pw.print(" from ");
                     UserHandle.formatUid(pw, js.getSourceUid());
-                    pw.print(" Num Required Constraints: ");
+                    pw.print("-> Num Required Constraints: ");
                     pw.print(js.getNumRequiredFlexibleConstraints());
+
+                    pw.print(", lifecycle=[");
+                    final long earliest = getLifeCycleBeginningElapsedLocked(js);
+                    pw.print(earliest);
+                    pw.print(", (");
+                    pw.print(getCurPercentOfLifecycleLocked(js, nowElapsed));
+                    pw.print("%), ");
+                    pw.print(getLifeCycleEndElapsedLocked(js, nowElapsed, earliest));
+                    pw.print("]");
+
                     pw.println();
                 }
             }
@@ -768,7 +971,7 @@
         public void scheduleDropNumConstraintsAlarm(JobStatus js, long nowElapsed) {
             synchronized (mLock) {
                 final long earliest = getLifeCycleBeginningElapsedLocked(js);
-                final long latest = getLifeCycleEndElapsedLocked(js, earliest);
+                final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest);
                 final long nextTimeElapsed =
                         getNextConstraintDropTimeElapsedLocked(js, earliest, latest);
 
@@ -936,10 +1139,16 @@
                 FC_CONFIG_PREFIX + "flexibility_deadline_proximity_limit_ms";
         static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE =
                 FC_CONFIG_PREFIX + "fallback_flexibility_deadline_ms";
+        static final String KEY_FALLBACK_FLEXIBILITY_DEADLINES =
+                FC_CONFIG_PREFIX + "fallback_flexibility_deadlines";
+        static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES =
+                FC_CONFIG_PREFIX + "fallback_flexibility_deadline_scores";
+        static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS =
+                FC_CONFIG_PREFIX + "fallback_flexibility_deadline_additional_score_time_factors";
         static final String KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS =
                 FC_CONFIG_PREFIX + "min_time_between_flexibility_alarms_ms";
-        static final String KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS =
-                FC_CONFIG_PREFIX + "percents_to_drop_num_flexible_constraints";
+        static final String KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS =
+                FC_CONFIG_PREFIX + "percents_to_drop_flexible_constraints";
         static final String KEY_MAX_RESCHEDULED_DEADLINE_MS =
                 FC_CONFIG_PREFIX + "max_rescheduled_deadline_ms";
         static final String KEY_RESCHEDULED_JOB_DEADLINE_MS =
@@ -952,9 +1161,50 @@
         static final long DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS;
         @VisibleForTesting
         static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 24 * HOUR_IN_MILLIS;
-        private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS;
+        static final SparseLongArray DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES = new SparseLongArray();
+        static final SparseIntArray DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES =
+                new SparseIntArray();
+        static final SparseLongArray
+                DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS =
+                new SparseLongArray();
         @VisibleForTesting
-        final int[] DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS = {50, 60, 70, 80};
+        static final SparseArray<int[]> DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS =
+                new SparseArray<>();
+
+        static {
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_MAX, HOUR_IN_MILLIS);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_HIGH, 6 * HOUR_IN_MILLIS);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_DEFAULT, 12 * HOUR_IN_MILLIS);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_LOW, 24 * HOUR_IN_MILLIS);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_MIN, 48 * HOUR_IN_MILLIS);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_MAX, 5);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_HIGH, 4);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_DEFAULT, 3);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_LOW, 2);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_MIN, 1);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
+                    .put(PRIORITY_MAX, 0);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
+                    .put(PRIORITY_HIGH, 4 * MINUTE_IN_MILLIS);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
+                    .put(PRIORITY_DEFAULT, 3 * MINUTE_IN_MILLIS);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
+                    .put(PRIORITY_LOW, 2 * MINUTE_IN_MILLIS);
+            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
+                    .put(PRIORITY_MIN, 1 * MINUTE_IN_MILLIS);
+            DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                    .put(PRIORITY_MAX, new int[]{1, 2, 3, 4});
+            DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                    .put(PRIORITY_HIGH, new int[]{33, 50, 60, 75});
+            DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                    .put(PRIORITY_DEFAULT, new int[]{50, 60, 70, 80});
+            DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                    .put(PRIORITY_LOW, new int[]{50, 60, 70, 80});
+            DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                    .put(PRIORITY_MIN, new int[]{55, 65, 75, 85});
+        }
+
+        private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS;
         private static final long DEFAULT_RESCHEDULED_JOB_DEADLINE_MS = HOUR_IN_MILLIS;
         private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = 5 * DAY_IN_MILLIS;
         @VisibleForTesting
@@ -968,9 +1218,11 @@
         public long FALLBACK_FLEXIBILITY_DEADLINE_MS = DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
         public long MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS =
                 DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS;
-        /** The percentages of a jobs' lifecycle to drop the number of required constraints. */
-        public int[] PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS =
-                DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+        /**
+         * The percentages of a jobs' lifecycle to drop the number of required constraints.
+         * Keyed by job priority.
+         */
+        public SparseArray<int[]> PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS = new SparseArray<>();
         /** Initial fallback flexible deadline for rescheduled jobs. */
         public long RESCHEDULED_JOB_DEADLINE_MS = DEFAULT_RESCHEDULED_JOB_DEADLINE_MS;
         /** The max deadline for rescheduled jobs. */
@@ -980,10 +1232,56 @@
          * it in order to run jobs.
          */
         public long UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
+        /**
+         * The base fallback deadlines to use if a job doesn't have its own deadline. Values are in
+         * milliseconds and keyed by job priority.
+         */
+        public final SparseLongArray FALLBACK_FLEXIBILITY_DEADLINES = new SparseLongArray();
+        /**
+         * The score to ascribe to each job, keyed by job priority.
+         */
+        public final SparseIntArray FALLBACK_FLEXIBILITY_DEADLINE_SCORES = new SparseIntArray();
+        /**
+         * How much additional time to increase the fallback deadline by based on the app's current
+         * job run score. Values are in
+         * milliseconds and keyed by job priority.
+         */
+        public final SparseLongArray FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS =
+                new SparseLongArray();
+
+        FcConfig() {
+            // Copy the values from the DEFAULT_* data structures to avoid accidentally modifying
+            // the DEFAULT_* data structures in other parts of the code.
+            for (int i = 0; i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.size(); ++i) {
+                FALLBACK_FLEXIBILITY_DEADLINES.put(
+                        DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.keyAt(i),
+                        DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.valueAt(i));
+            }
+            for (int i = 0; i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.size(); ++i) {
+                FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(
+                        DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.keyAt(i),
+                        DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.valueAt(i));
+            }
+            for (int i = 0;
+                    i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS.size();
+                    ++i) {
+                FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS.put(
+                        DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
+                                .keyAt(i),
+                        DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
+                                .valueAt(i));
+            }
+            for (int i = 0; i < DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.size(); ++i) {
+                PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.put(
+                        DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.keyAt(i),
+                        DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.valueAt(i));
+            }
+        }
 
         @GuardedBy("mLock")
         public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
                 @NonNull String key) {
+            // TODO(257322915): add appropriate minimums and maximums to constants when parsing
             switch (key) {
                 case KEY_APPLIED_CONSTRAINTS:
                     APPLIED_CONSTRAINTS =
@@ -1034,6 +1332,33 @@
                             properties.getLong(key, DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS);
                     if (mFallbackFlexibilityDeadlineMs != FALLBACK_FLEXIBILITY_DEADLINE_MS) {
                         mFallbackFlexibilityDeadlineMs = FALLBACK_FLEXIBILITY_DEADLINE_MS;
+                    }
+                    break;
+                case KEY_FALLBACK_FLEXIBILITY_DEADLINES:
+                    if (parsePriorityToLongKeyValueString(
+                            properties.getString(key, null),
+                            FALLBACK_FLEXIBILITY_DEADLINES,
+                            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES)) {
+                        mFallbackFlexibilityDeadlines = FALLBACK_FLEXIBILITY_DEADLINES;
+                        mShouldReevaluateConstraints = true;
+                    }
+                    break;
+                case KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES:
+                    if (parsePriorityToIntKeyValueString(
+                            properties.getString(key, null),
+                            FALLBACK_FLEXIBILITY_DEADLINE_SCORES,
+                            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES)) {
+                        mFallbackFlexibilityDeadlineScores = FALLBACK_FLEXIBILITY_DEADLINE_SCORES;
+                        mShouldReevaluateConstraints = true;
+                    }
+                    break;
+                case KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS:
+                    if (parsePriorityToLongKeyValueString(
+                            properties.getString(key, null),
+                            FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS,
+                            DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS)) {
+                        mFallbackFlexibilityAdditionalScoreTimeFactors =
+                                FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS;
                         mShouldReevaluateConstraints = true;
                     }
                     break;
@@ -1056,25 +1381,69 @@
                         mShouldReevaluateConstraints = true;
                     }
                     break;
-                case KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS:
-                    String dropPercentString = properties.getString(key, "");
-                    PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS =
-                            parsePercentToDropString(dropPercentString);
-                    if (PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS != null
-                            && !Arrays.equals(mPercentToDropConstraints,
-                            PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS)) {
-                        mPercentToDropConstraints = PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS;
+                case KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS:
+                    if (parsePercentToDropKeyValueString(
+                            properties.getString(key, null),
+                            PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
+                            DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS)) {
+                        mPercentsToDropConstraints = PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS;
                         mShouldReevaluateConstraints = true;
                     }
                     break;
             }
         }
 
-        private int[] parsePercentToDropString(String s) {
-            String[] dropPercentString = s.split(",");
+        private boolean parsePercentToDropKeyValueString(@Nullable String s,
+                SparseArray<int[]> into, SparseArray<int[]> defaults) {
+            final KeyValueListParser priorityParser = new KeyValueListParser(',');
+            try {
+                priorityParser.setString(s);
+            } catch (IllegalArgumentException e) {
+                Slog.wtf(TAG, "Bad percent to drop key value string given", e);
+                // Clear the string and continue with the defaults.
+                priorityParser.setString(null);
+            }
+
+            final int[] oldMax = into.get(PRIORITY_MAX);
+            final int[] oldHigh = into.get(PRIORITY_HIGH);
+            final int[] oldDefault = into.get(PRIORITY_DEFAULT);
+            final int[] oldLow = into.get(PRIORITY_LOW);
+            final int[] oldMin = into.get(PRIORITY_MIN);
+
+            final int[] newMax = parsePercentToDropString(priorityParser.getString(
+                    String.valueOf(PRIORITY_MAX), null));
+            final int[] newHigh = parsePercentToDropString(priorityParser.getString(
+                    String.valueOf(PRIORITY_HIGH), null));
+            final int[] newDefault = parsePercentToDropString(priorityParser.getString(
+                    String.valueOf(PRIORITY_DEFAULT), null));
+            final int[] newLow = parsePercentToDropString(priorityParser.getString(
+                    String.valueOf(PRIORITY_LOW), null));
+            final int[] newMin = parsePercentToDropString(priorityParser.getString(
+                    String.valueOf(PRIORITY_MIN), null));
+
+            into.put(PRIORITY_MAX, newMax == null ? defaults.get(PRIORITY_MAX) : newMax);
+            into.put(PRIORITY_HIGH, newHigh == null ? defaults.get(PRIORITY_HIGH) : newHigh);
+            into.put(PRIORITY_DEFAULT,
+                    newDefault == null ? defaults.get(PRIORITY_DEFAULT) : newDefault);
+            into.put(PRIORITY_LOW, newLow == null ? defaults.get(PRIORITY_LOW) : newLow);
+            into.put(PRIORITY_MIN, newMin == null ? defaults.get(PRIORITY_MIN) : newMin);
+
+            return !Arrays.equals(oldMax, into.get(PRIORITY_MAX))
+                    || !Arrays.equals(oldHigh, into.get(PRIORITY_HIGH))
+                    || !Arrays.equals(oldDefault, into.get(PRIORITY_DEFAULT))
+                    || !Arrays.equals(oldLow, into.get(PRIORITY_LOW))
+                    || !Arrays.equals(oldMin, into.get(PRIORITY_MIN));
+        }
+
+        @Nullable
+        private int[] parsePercentToDropString(@Nullable String s) {
+            if (s == null || s.isEmpty()) {
+                return null;
+            }
+            final String[] dropPercentString = s.split("\\|");
             int[] dropPercentInt = new int[Integer.bitCount(FLEXIBLE_CONSTRAINTS)];
             if (dropPercentInt.length != dropPercentString.length) {
-                return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+                return null;
             }
             int prevPercent = 0;
             for (int i = 0; i < dropPercentString.length; i++) {
@@ -1083,11 +1452,15 @@
                             Integer.parseInt(dropPercentString[i]);
                 } catch (NumberFormatException ex) {
                     Slog.e(TAG, "Provided string was improperly formatted.", ex);
-                    return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+                    return null;
                 }
                 if (dropPercentInt[i] < prevPercent) {
                     Slog.wtf(TAG, "Percents to drop constraints were not in increasing order.");
-                    return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+                    return null;
+                }
+                if (dropPercentInt[i] > 100) {
+                    Slog.e(TAG, "Found % over 100");
+                    return null;
                 }
                 prevPercent = dropPercentInt[i];
             }
@@ -1095,6 +1468,102 @@
             return dropPercentInt;
         }
 
+        /**
+         * Parses the input string, expecting it to a key-value string where the keys are job
+         * priorities, and replaces everything in {@code into} with the values from the string,
+         * or the default values if the string contains none.
+         *
+         * Returns true if any values changed.
+         */
+        private boolean parsePriorityToIntKeyValueString(@Nullable String s,
+                SparseIntArray into, SparseIntArray defaults) {
+            final KeyValueListParser parser = new KeyValueListParser(',');
+            try {
+                parser.setString(s);
+            } catch (IllegalArgumentException e) {
+                Slog.wtf(TAG, "Bad string given", e);
+                // Clear the string and continue with the defaults.
+                parser.setString(null);
+            }
+
+            final int oldMax = into.get(PRIORITY_MAX);
+            final int oldHigh = into.get(PRIORITY_HIGH);
+            final int oldDefault = into.get(PRIORITY_DEFAULT);
+            final int oldLow = into.get(PRIORITY_LOW);
+            final int oldMin = into.get(PRIORITY_MIN);
+
+            final int newMax = parser.getInt(String.valueOf(PRIORITY_MAX),
+                    defaults.get(PRIORITY_MAX));
+            final int newHigh = parser.getInt(String.valueOf(PRIORITY_HIGH),
+                    defaults.get(PRIORITY_HIGH));
+            final int newDefault = parser.getInt(String.valueOf(PRIORITY_DEFAULT),
+                    defaults.get(PRIORITY_DEFAULT));
+            final int newLow = parser.getInt(String.valueOf(PRIORITY_LOW),
+                    defaults.get(PRIORITY_LOW));
+            final int newMin = parser.getInt(String.valueOf(PRIORITY_MIN),
+                    defaults.get(PRIORITY_MIN));
+
+            into.put(PRIORITY_MAX, newMax);
+            into.put(PRIORITY_HIGH, newHigh);
+            into.put(PRIORITY_DEFAULT, newDefault);
+            into.put(PRIORITY_LOW, newLow);
+            into.put(PRIORITY_MIN, newMin);
+
+            return oldMax != newMax
+                    || oldHigh != newHigh
+                    || oldDefault != newDefault
+                    || oldLow != newLow
+                    || oldMin != newMin;
+        }
+
+        /**
+         * Parses the input string, expecting it to a key-value string where the keys are job
+         * priorities, and replaces everything in {@code into} with the values from the string,
+         * or the default values if the string contains none.
+         *
+         * Returns true if any values changed.
+         */
+        private boolean parsePriorityToLongKeyValueString(@Nullable String s,
+                SparseLongArray into, SparseLongArray defaults) {
+            final KeyValueListParser parser = new KeyValueListParser(',');
+            try {
+                parser.setString(s);
+            } catch (IllegalArgumentException e) {
+                Slog.wtf(TAG, "Bad string given", e);
+                // Clear the string and continue with the defaults.
+                parser.setString(null);
+            }
+
+            final long oldMax = into.get(PRIORITY_MAX);
+            final long oldHigh = into.get(PRIORITY_HIGH);
+            final long oldDefault = into.get(PRIORITY_DEFAULT);
+            final long oldLow = into.get(PRIORITY_LOW);
+            final long oldMin = into.get(PRIORITY_MIN);
+
+            final long newMax = parser.getLong(String.valueOf(PRIORITY_MAX),
+                    defaults.get(PRIORITY_MAX));
+            final long newHigh = parser.getLong(String.valueOf(PRIORITY_HIGH),
+                    defaults.get(PRIORITY_HIGH));
+            final long newDefault = parser.getLong(String.valueOf(PRIORITY_DEFAULT),
+                    defaults.get(PRIORITY_DEFAULT));
+            final long newLow = parser.getLong(String.valueOf(PRIORITY_LOW),
+                    defaults.get(PRIORITY_LOW));
+            final long newMin = parser.getLong(String.valueOf(PRIORITY_MIN),
+                    defaults.get(PRIORITY_MIN));
+
+            into.put(PRIORITY_MAX, newMax);
+            into.put(PRIORITY_HIGH, newHigh);
+            into.put(PRIORITY_DEFAULT, newDefault);
+            into.put(PRIORITY_LOW, newLow);
+            into.put(PRIORITY_MIN, newMin);
+
+            return oldMax != newMax
+                    || oldHigh != newHigh
+                    || oldDefault != newDefault
+                    || oldLow != newLow
+                    || oldMin != newMin;
+        }
+
         private void dump(IndentingPrintWriter pw) {
             pw.println();
             pw.print(FlexibilityController.class.getSimpleName());
@@ -1111,10 +1580,15 @@
             pw.println(")");
             pw.print(KEY_DEADLINE_PROXIMITY_LIMIT, DEADLINE_PROXIMITY_LIMIT_MS).println();
             pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE, FALLBACK_FLEXIBILITY_DEADLINE_MS).println();
+            pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINES, FALLBACK_FLEXIBILITY_DEADLINES).println();
+            pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES,
+                    FALLBACK_FLEXIBILITY_DEADLINE_SCORES).println();
+            pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS,
+                    FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS).println();
             pw.print(KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS,
                     MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS).println();
-            pw.print(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS,
-                    PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS).println();
+            pw.print(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
+                    PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS).println();
             pw.print(KEY_RESCHEDULED_JOB_DEADLINE_MS, RESCHEDULED_JOB_DEADLINE_MS).println();
             pw.print(KEY_MAX_RESCHEDULED_DEADLINE_MS, MAX_RESCHEDULED_DEADLINE_MS).println();
             pw.print(KEY_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS, UNSEEN_CONSTRAINT_GRACE_PERIOD_MS)
@@ -1171,7 +1645,21 @@
         pw.println(mPowerAllowlistedApps);
 
         pw.println();
-        mFlexibilityTracker.dump(pw, predicate);
+        mFlexibilityTracker.dump(pw, predicate, nowElapsed);
+
+        pw.println();
+        pw.println("Job scores:");
+        pw.increaseIndent();
+        mJobScoreTrackers.forEach((uid, pkgName, jobScoreTracker) -> {
+            pw.print(uid);
+            pw.print("/");
+            pw.print(pkgName);
+            pw.print(": ");
+            jobScoreTracker.dump(pw, nowElapsed);
+            pw.println();
+        });
+        pw.decreaseIndent();
+
         pw.println();
         mFlexibilityAlarmQueue.dump(pw);
     }
diff --git a/config/preloaded-classes b/config/preloaded-classes
index c49971e..11b24f5 100644
--- a/config/preloaded-classes
+++ b/config/preloaded-classes
@@ -6172,8 +6172,6 @@
 android.os.VibratorInfo$FrequencyProfile
 android.os.VibratorInfo
 android.os.VibratorManager
-android.os.VintfObject
-android.os.VintfRuntimeInfo
 android.os.WorkSource$1
 android.os.WorkSource$WorkChain$1
 android.os.WorkSource$WorkChain
diff --git a/core/api/current.txt b/core/api/current.txt
index 49b6d25..671956d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -195,6 +195,7 @@
     field public static final String MANAGE_DEVICE_POLICY_SYSTEM_APPS = "android.permission.MANAGE_DEVICE_POLICY_SYSTEM_APPS";
     field public static final String MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS = "android.permission.MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS";
     field public static final String MANAGE_DEVICE_POLICY_SYSTEM_UPDATES = "android.permission.MANAGE_DEVICE_POLICY_SYSTEM_UPDATES";
+    field @FlaggedApi("com.android.net.thread.flags.thread_user_restriction_enabled") public static final String MANAGE_DEVICE_POLICY_THREAD_NETWORK = "android.permission.MANAGE_DEVICE_POLICY_THREAD_NETWORK";
     field public static final String MANAGE_DEVICE_POLICY_TIME = "android.permission.MANAGE_DEVICE_POLICY_TIME";
     field public static final String MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING = "android.permission.MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING";
     field public static final String MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER = "android.permission.MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER";
@@ -12446,6 +12447,7 @@
     method @NonNull public android.content.pm.PackageInstaller.Session openSession(int) throws java.io.IOException;
     method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback);
     method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback, @NonNull android.os.Handler);
+    method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalState(@NonNull android.content.pm.PackageInstaller.UnarchivalState) throws android.content.pm.PackageManager.NameNotFoundException;
     method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalStatus(int, int, long, @Nullable android.app.PendingIntent) throws android.content.pm.PackageManager.NameNotFoundException;
     method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException;
     method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String, @NonNull android.content.IntentSender) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException;
@@ -12675,6 +12677,14 @@
     field public static final int USER_ACTION_UNSPECIFIED = 0; // 0x0
   }
 
+  @FlaggedApi("android.content.pm.archiving") public static final class PackageInstaller.UnarchivalState {
+    method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createGenericErrorState(int);
+    method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createInsufficientStorageState(int, long, @Nullable android.app.PendingIntent);
+    method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createNoConnectivityState(int);
+    method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createOkState(int);
+    method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createUserActionRequiredState(int, @NonNull android.app.PendingIntent);
+  }
+
   public class PackageItemInfo {
     ctor public PackageItemInfo();
     ctor public PackageItemInfo(android.content.pm.PackageItemInfo);
@@ -17875,6 +17885,7 @@
     field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2
     field public static final int HYPHENATION_FREQUENCY_NONE = 0; // 0x0
     field public static final int HYPHENATION_FREQUENCY_NORMAL = 1; // 0x1
+    field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
     field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
     field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
   }
@@ -22153,16 +22164,17 @@
     method public void onJetUserIdUpdate(android.media.JetPlayer, int, int);
   }
 
-  @FlaggedApi("android.media.audio.loudness_configurator_api") public class LoudnessCodecConfigurator {
+  @FlaggedApi("android.media.audio.loudness_configurator_api") public class LoudnessCodecController implements java.lang.AutoCloseable {
     method @FlaggedApi("android.media.audio.loudness_configurator_api") public boolean addMediaCodec(@NonNull android.media.MediaCodec);
-    method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecConfigurator create();
-    method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecConfigurator create(@NonNull java.util.concurrent.Executor, @NonNull android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener);
-    method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public android.os.Bundle getLoudnessCodecParams(@NonNull android.media.AudioTrack, @NonNull android.media.MediaCodec);
+    method public void close();
+    method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecController create(int);
+    method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecController create(int, @NonNull java.util.concurrent.Executor, @NonNull android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener);
+    method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public android.os.Bundle getLoudnessCodecParams(@NonNull android.media.MediaCodec);
+    method @FlaggedApi("android.media.audio.loudness_configurator_api") public void release();
     method @FlaggedApi("android.media.audio.loudness_configurator_api") public void removeMediaCodec(@NonNull android.media.MediaCodec);
-    method @FlaggedApi("android.media.audio.loudness_configurator_api") public void setAudioTrack(@Nullable android.media.AudioTrack);
   }
 
-  @FlaggedApi("android.media.audio.loudness_configurator_api") public static interface LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener {
+  @FlaggedApi("android.media.audio.loudness_configurator_api") public static interface LoudnessCodecController.OnLoudnessCodecUpdateListener {
     method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public default android.os.Bundle onLoudnessCodecUpdate(@NonNull android.media.MediaCodec, @NonNull android.os.Bundle);
   }
 
@@ -23316,6 +23328,8 @@
     field public static final String KEY_AUDIO_SESSION_ID = "audio-session-id";
     field public static final String KEY_BITRATE_MODE = "bitrate-mode";
     field public static final String KEY_BIT_RATE = "bitrate";
+    field @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public static final String KEY_BUFFER_BATCH_MAX_OUTPUT_SIZE = "buffer-batch-max-output-size";
+    field @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public static final String KEY_BUFFER_BATCH_THRESHOLD_OUTPUT_SIZE = "buffer-batch-threshold-output-size";
     field public static final String KEY_CAPTION_SERVICE_NUMBER = "caption-service-number";
     field public static final String KEY_CAPTURE_RATE = "capture-rate";
     field public static final String KEY_CHANNEL_COUNT = "channel-count";
@@ -25472,34 +25486,34 @@
     field public short preset;
   }
 
-  public class Virtualizer extends android.media.audiofx.AudioEffect {
-    ctor public Virtualizer(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException;
-    method public boolean canVirtualize(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
-    method public boolean forceVirtualizationMode(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
-    method public android.media.audiofx.Virtualizer.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
-    method public short getRoundedStrength() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
-    method public boolean getSpeakerAngles(int, int, int[]) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
-    method public boolean getStrengthSupported();
-    method public int getVirtualizationMode() throws java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
-    method public void setParameterListener(android.media.audiofx.Virtualizer.OnParameterChangeListener);
-    method public void setProperties(android.media.audiofx.Virtualizer.Settings) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
-    method public void setStrength(short) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
-    field public static final int PARAM_STRENGTH = 1; // 0x1
-    field public static final int PARAM_STRENGTH_SUPPORTED = 0; // 0x0
-    field public static final int VIRTUALIZATION_MODE_AUTO = 1; // 0x1
-    field public static final int VIRTUALIZATION_MODE_BINAURAL = 2; // 0x2
-    field public static final int VIRTUALIZATION_MODE_OFF = 0; // 0x0
-    field public static final int VIRTUALIZATION_MODE_TRANSAURAL = 3; // 0x3
+  @Deprecated public class Virtualizer extends android.media.audiofx.AudioEffect {
+    ctor @Deprecated public Virtualizer(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException;
+    method @Deprecated public boolean canVirtualize(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+    method @Deprecated public boolean forceVirtualizationMode(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+    method @Deprecated public android.media.audiofx.Virtualizer.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+    method @Deprecated public short getRoundedStrength() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+    method @Deprecated public boolean getSpeakerAngles(int, int, int[]) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+    method @Deprecated public boolean getStrengthSupported();
+    method @Deprecated public int getVirtualizationMode() throws java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+    method @Deprecated public void setParameterListener(android.media.audiofx.Virtualizer.OnParameterChangeListener);
+    method @Deprecated public void setProperties(android.media.audiofx.Virtualizer.Settings) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+    method @Deprecated public void setStrength(short) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+    field @Deprecated public static final int PARAM_STRENGTH = 1; // 0x1
+    field @Deprecated public static final int PARAM_STRENGTH_SUPPORTED = 0; // 0x0
+    field @Deprecated public static final int VIRTUALIZATION_MODE_AUTO = 1; // 0x1
+    field @Deprecated public static final int VIRTUALIZATION_MODE_BINAURAL = 2; // 0x2
+    field @Deprecated public static final int VIRTUALIZATION_MODE_OFF = 0; // 0x0
+    field @Deprecated public static final int VIRTUALIZATION_MODE_TRANSAURAL = 3; // 0x3
   }
 
-  public static interface Virtualizer.OnParameterChangeListener {
-    method public void onParameterChange(android.media.audiofx.Virtualizer, int, int, short);
+  @Deprecated public static interface Virtualizer.OnParameterChangeListener {
+    method @Deprecated public void onParameterChange(android.media.audiofx.Virtualizer, int, int, short);
   }
 
-  public static class Virtualizer.Settings {
-    ctor public Virtualizer.Settings();
-    ctor public Virtualizer.Settings(String);
-    field public short strength;
+  @Deprecated public static class Virtualizer.Settings {
+    ctor @Deprecated public Virtualizer.Settings();
+    ctor @Deprecated public Virtualizer.Settings(String);
+    field @Deprecated public short strength;
   }
 
   public class Visualizer {
@@ -42218,9 +42232,11 @@
     method public android.graphics.drawable.Icon getIcon();
     method public CharSequence getLabel();
     method public CharSequence getShortDescription();
+    method @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @NonNull public java.util.Set<android.telecom.PhoneAccountHandle> getSimultaneousCallingRestriction();
     method public android.net.Uri getSubscriptionAddress();
     method public java.util.List<java.lang.String> getSupportedUriSchemes();
     method public boolean hasCapabilities(int);
+    method @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") public boolean hasSimultaneousCallingRestriction();
     method public boolean isEnabled();
     method public boolean supportsUriScheme(String);
     method public android.telecom.PhoneAccount.Builder toBuilder();
@@ -42261,12 +42277,14 @@
     ctor public PhoneAccount.Builder(android.telecom.PhoneAccount);
     method public android.telecom.PhoneAccount.Builder addSupportedUriScheme(String);
     method public android.telecom.PhoneAccount build();
+    method @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @NonNull public android.telecom.PhoneAccount.Builder clearSimultaneousCallingRestriction();
     method public android.telecom.PhoneAccount.Builder setAddress(android.net.Uri);
     method public android.telecom.PhoneAccount.Builder setCapabilities(int);
     method public android.telecom.PhoneAccount.Builder setExtras(android.os.Bundle);
     method public android.telecom.PhoneAccount.Builder setHighlightColor(int);
     method public android.telecom.PhoneAccount.Builder setIcon(android.graphics.drawable.Icon);
     method public android.telecom.PhoneAccount.Builder setShortDescription(CharSequence);
+    method @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @NonNull public android.telecom.PhoneAccount.Builder setSimultaneousCallingRestriction(@NonNull java.util.Set<android.telecom.PhoneAccountHandle>);
     method public android.telecom.PhoneAccount.Builder setSubscriptionAddress(android.net.Uri);
     method public android.telecom.PhoneAccount.Builder setSupportedUriSchemes(java.util.List<java.lang.String>);
   }
@@ -44944,7 +44962,7 @@
     method public void addOnSubscriptionsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void addSubscriptionsIntoGroup(@NonNull java.util.List<java.lang.Integer>, @NonNull android.os.ParcelUuid);
     method public boolean canManageSubscription(android.telephony.SubscriptionInfo);
-    method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull public android.telephony.SubscriptionManager createForAllUserProfiles();
+    method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) public android.telephony.SubscriptionManager createForAllUserProfiles();
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.os.ParcelUuid createSubscriptionGroup(@NonNull java.util.List<java.lang.Integer>);
     method @Deprecated public static android.telephony.SubscriptionManager from(android.content.Context);
     method public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList();
@@ -46947,6 +46965,7 @@
     field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP;
     field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL;
     field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER;
+    field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
     field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
     field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
   }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index debf1bf..fe32bad 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3959,6 +3959,7 @@
     method public void setInstallAsInstantApp(boolean);
     method public void setInstallAsVirtualPreload();
     method public void setRequestDowngrade(boolean);
+    method @FlaggedApi("android.content.pm.recoverability_detection") @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) public void setRollbackImpactLevel(int);
     method @FlaggedApi("android.content.pm.rollback_lifetime") @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) public void setRollbackLifetimeMillis(long);
     method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setStaged();
   }
@@ -4127,6 +4128,9 @@
     field public static final int ROLLBACK_DATA_POLICY_RESTORE = 0; // 0x0
     field public static final int ROLLBACK_DATA_POLICY_RETAIN = 2; // 0x2
     field public static final int ROLLBACK_DATA_POLICY_WIPE = 1; // 0x1
+    field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_HIGH = 1; // 0x1
+    field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_LOW = 0; // 0x0
+    field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_ONLY_MANUAL = 2; // 0x2
     field public static final int SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_HIDDEN = 0; // 0x0
     field public static final int SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_VISIBLE = 1; // 0x1
     field public static final int SYSTEM_APP_STATE_INSTALLED = 2; // 0x2
@@ -14712,7 +14716,7 @@
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion();
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain();
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimIst();
-    method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"}) public android.telephony.CellIdentity getLastKnownCellIdentity();
+    method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID}) public android.telephony.CellIdentity getLastKnownCellIdentity();
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Map<java.lang.Integer,java.lang.Integer> getLogicalToPhysicalSlotMapping();
     method public int getMaxNumberOfSimultaneouslyActiveSims();
     method public static long getMaxNumberVerificationTimeoutMillis();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index a866a34..b8b98a3 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -156,6 +156,7 @@
     field public static final int PROCESS_CAPABILITY_ALL_IMPLICIT = 6; // 0x6
     field public static final int PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK = 8; // 0x8
     field public static final int PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK = 32; // 0x20
+    field public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 5; // 0x5
     field public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4; // 0x4
     field public static final int PROCESS_STATE_TOP = 2; // 0x2
     field public static final int STOP_USER_ON_SWITCH_DEFAULT = -1; // 0xffffffff
@@ -376,6 +377,7 @@
   public class NotificationManager {
     method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean);
     method public void cleanUpCallersAfter(long);
+    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy();
     method public android.content.ComponentName getEffectsSuppressor();
     method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
     method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
@@ -1209,6 +1211,10 @@
 
 package android.content.rollback {
 
+  public final class RollbackInfo implements android.os.Parcelable {
+    method @FlaggedApi("android.content.pm.recoverability_detection") public int getRollbackImpactLevel();
+  }
+
   public final class RollbackManager {
     method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void blockRollbackManager(long);
     method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void expireRollbackForPackage(@NonNull String);
@@ -3022,6 +3028,10 @@
     method @Deprecated public boolean isBound();
   }
 
+  public final class ZenPolicy implements android.os.Parcelable {
+    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy);
+  }
+
   public static final class ZenPolicy.Builder {
     ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy);
   }
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index b2c6475..9d20f3c 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -712,6 +712,8 @@
 
     /** @hide Process is hosting a foreground service due to a system binding. */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+    @TestApi
     public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE =
             ProcessStateEnum.BOUND_FOREGROUND_SERVICE;
 
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 4b2e93f..d8d136a 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1533,10 +1533,11 @@
     public static final int OP_RESERVED_FOR_TESTING = AppProtoEnums.APP_OP_RESERVED_FOR_TESTING;
 
     /**
-     * Rapid clearing of notifications by a notification listener, see b/289080543 for details
+     * Rapid clearing of notifications by a notification listener
      *
      * @hide
      */
+    // See b/289080543 for more details
     public static final int OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER =
             AppProtoEnums.APP_OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER;
 
@@ -2373,10 +2374,11 @@
             "android:reserved_for_testing";
 
     /**
-     * Rapid clearing of notifications by a notification listener, see b/289080543 for details
+     * Rapid clearing of notifications by a notification listener
      *
      * @hide
      */
+    // See b/289080543 for more details
     @SystemApi
     @FlaggedApi(FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED)
     public static final String OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER =
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index afa513d..c6712c0 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -413,7 +413,9 @@
      * @hide
      */
     public void setIntent(Intent startIntent) {
-        mStartIntent = startIntent;
+        if (startIntent != null) {
+            mStartIntent = startIntent.maybeStripForHistory();
+        }
     }
 
     /**
@@ -548,6 +550,8 @@
     /**
      * The intent used to launch the application.
      *
+     * <p class="note"> Note: Intent is stripped and does not include extras.</p>
+     *
      * <p class="note"> Note: field will be set for any {@link #getStartupState} value.</p>
      */
     @SuppressLint("IntentBuilderName")
@@ -662,6 +666,7 @@
     private static final String PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP = "timestamp";
     private static final String PROTO_SERIALIZER_ATTRIBUTE_KEY = "key";
     private static final String PROTO_SERIALIZER_ATTRIBUTE_TS = "ts";
+    private static final String PROTO_SERIALIZER_ATTRIBUTE_INTENT = "intent";
 
     /**
      * Write to a protocol buffer output stream. Protocol buffer message definition at {@link
@@ -702,10 +707,17 @@
         }
         proto.write(ApplicationStartInfoProto.START_TYPE, mStartType);
         if (mStartIntent != null) {
-            Parcel parcel = Parcel.obtain();
-            mStartIntent.writeToParcel(parcel, 0);
-            proto.write(ApplicationStartInfoProto.START_INTENT, parcel.marshall());
-            parcel.recycle();
+            ByteArrayOutputStream intentBytes = new ByteArrayOutputStream();
+            ObjectOutputStream intentOut = new ObjectOutputStream(intentBytes);
+            TypedXmlSerializer serializer = Xml.resolveSerializer(intentOut);
+            serializer.startDocument(null, true);
+            serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+            mStartIntent.saveToXml(serializer);
+            serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+            serializer.endDocument();
+            proto.write(ApplicationStartInfoProto.START_INTENT,
+                    intentBytes.toByteArray());
+            intentOut.close();
         }
         proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode);
         proto.end(token);
@@ -772,15 +784,17 @@
                     mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE);
                     break;
                 case (int) ApplicationStartInfoProto.START_INTENT:
-                    byte[] startIntentBytes = proto.readBytes(
-                        ApplicationStartInfoProto.START_INTENT);
-                    if (startIntentBytes.length > 0) {
-                        Parcel parcel = Parcel.obtain();
-                        parcel.unmarshall(startIntentBytes, 0, startIntentBytes.length);
-                        parcel.setDataPosition(0);
-                        mStartIntent = Intent.CREATOR.createFromParcel(parcel);
-                        parcel.recycle();
+                    ByteArrayInputStream intentBytes = new ByteArrayInputStream(proto.readBytes(
+                            ApplicationStartInfoProto.START_INTENT));
+                    ObjectInputStream intentIn = new ObjectInputStream(intentBytes);
+                    try {
+                        TypedXmlPullParser parser = Xml.resolvePullParser(intentIn);
+                        XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+                        mStartIntent = Intent.restoreFromXml(parser);
+                    } catch (XmlPullParserException e) {
+                        // Intent lost
                     }
+                    intentIn.close();
                     break;
                 case (int) ApplicationStartInfoProto.LAUNCH_MODE:
                     mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE);
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 7370fc3..5b044f6 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -119,7 +119,7 @@
 
     oneway void setShowWhenLocked(in IBinder token, boolean showWhenLocked);
     oneway void setInheritShowWhenLocked(in IBinder token, boolean setInheritShownWhenLocked);
-    oneway void setTurnScreenOn(in IBinder token, boolean turnScreenOn);
+    void setTurnScreenOn(in IBinder token, boolean turnScreenOn);
     oneway void setAllowCrossUidActivitySwitchFromBelow(in IBinder token, boolean allowed);
     oneway void reportActivityFullyDrawn(in IBinder token, boolean restoredFromBundle);
     oneway void overrideActivityTransition(IBinder token, boolean open, int enterAnim, int exitAnim,
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index c3adbc3..578105f 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -38,6 +38,7 @@
 import android.service.notification.INotificationListener;
 import android.service.notification.NotificationListenerFilter;
 import android.service.notification.StatusBarNotification;
+import android.service.notification.ZenPolicy;
 import android.app.AutomaticZenRule;
 import android.service.notification.ZenModeConfig;
 
@@ -213,6 +214,7 @@
     boolean isNotificationPolicyAccessGrantedForPackage(String pkg);
     void setNotificationPolicyAccessGranted(String pkg, boolean granted);
     void setNotificationPolicyAccessGrantedForUser(String pkg, int userId, boolean granted);
+    ZenPolicy getDefaultZenPolicy();
     AutomaticZenRule getAutomaticZenRule(String id);
     Map<String, AutomaticZenRule> getAutomaticZenRules();
     // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 0b6e24c..366b45b 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1750,6 +1750,20 @@
             @NonNull ComponentName listener, boolean granted) {
         setNotificationListenerAccessGranted(listener, granted, true);
     }
+    /**
+     * Gets the device-default notification policy as a ZenPolicy.
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public @NonNull ZenPolicy getDefaultZenPolicy() {
+        INotificationManager service = getService();
+        try {
+            return service.getDefaultZenPolicy();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 
     /**
      * For apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, the
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 63f37f1..36b03c1 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -261,6 +261,13 @@
     public static final String COMMAND_GOING_TO_SLEEP = "android.wallpaper.goingtosleep";
 
     /**
+     * Command for {@link #sendWallpaperCommand}: reported when a physical display switch event
+     * happens, e.g. fold and unfold.
+     * @hide
+     */
+    public static final String COMMAND_DISPLAY_SWITCH = "android.wallpaper.displayswitch";
+
+    /**
      * Command for {@link #sendWallpaperCommand}: reported when the wallpaper that was already
      * set is re-applied by the user.
      * @hide
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 35ce102..b3ecd92 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -55,3 +55,10 @@
   description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped."
   bug: "293441361"
 }
+
+flag {
+    name: "default_sms_personal_app_suspension_fix_enabled"
+    namespace: "enterprise"
+    description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended"
+    bug: "309183330"
+}
diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig
new file mode 100644
index 0000000..029b93a
--- /dev/null
+++ b/core/java/android/app/background_install_control_manager.aconfig
@@ -0,0 +1,9 @@
+package: "android.app"
+
+flag {
+     namespace: "background_install_control"
+     name: "bic_client"
+     description: "System API for background install control."
+     is_fixed_read_only: true
+     bug: "287507984"
+}
diff --git a/core/java/android/app/backup/BackupHelperWithLogger.java b/core/java/android/app/backup/BackupHelperWithLogger.java
new file mode 100644
index 0000000..1a59a53
--- /dev/null
+++ b/core/java/android/app/backup/BackupHelperWithLogger.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Utility class for writing BackupHelpers with added logging capabilities.
+ * Used for passing a logger object to Helper in key shared backup agents
+ *
+ * @hide
+ */
+public abstract class BackupHelperWithLogger implements BackupHelper {
+    private BackupRestoreEventLogger mLogger;
+    private boolean mIsLoggerSet = false;
+
+    public abstract void writeNewStateDescription(ParcelFileDescriptor newState);
+
+    public abstract void restoreEntity(BackupDataInputStream data);
+
+    public abstract void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+            ParcelFileDescriptor newState);
+
+    /**
+     * Gets the logger so that the backuphelper can log success/error for each datatype handled
+     */
+    public BackupRestoreEventLogger getLogger() {
+        return mLogger;
+    }
+
+    /**
+     * Allow the shared backup agent to pass a logger to each of its backup helper
+     */
+    public void setLogger(BackupRestoreEventLogger logger) {
+        mLogger = logger;
+        mIsLoggerSet = true;
+    }
+
+    /**
+     * Allow the helper to check if its shared backup agent has passed a logger
+     */
+    public boolean isLoggerSet() {
+        return mIsLoggerSet;
+    }
+}
diff --git a/core/java/android/app/backup/BlobBackupHelper.java b/core/java/android/app/backup/BlobBackupHelper.java
index 82d0a94c..a55ff48 100644
--- a/core/java/android/app/backup/BlobBackupHelper.java
+++ b/core/java/android/app/backup/BlobBackupHelper.java
@@ -39,7 +39,7 @@
  *
  * @hide
  */
-public abstract class BlobBackupHelper implements BackupHelper {
+public abstract class BlobBackupHelper extends BackupHelperWithLogger {
     private static final String TAG = "BlobBackupHelper";
     private static final boolean DEBUG = false;
 
diff --git a/core/java/android/content/pm/ArchivedActivityInfo.java b/core/java/android/content/pm/ArchivedActivityInfo.java
index 1faa437..166d265 100644
--- a/core/java/android/content/pm/ArchivedActivityInfo.java
+++ b/core/java/android/content/pm/ArchivedActivityInfo.java
@@ -91,26 +91,31 @@
      * @hide
      */
     public static Bitmap drawableToBitmap(Drawable drawable, int iconSize) {
-        if (drawable instanceof BitmapDrawable) {
-            return ((BitmapDrawable) drawable).getBitmap();
-
-        }
-
         Bitmap bitmap;
-        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
-            // Needed for drawables that are just a single color.
-            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+        if (drawable instanceof BitmapDrawable) {
+            bitmap = ((BitmapDrawable) drawable).getBitmap();
         } else {
-            bitmap =
+            if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+                // Needed for drawables that are just a single color.
+                bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+            } else {
+                bitmap =
                     Bitmap.createBitmap(
-                            drawable.getIntrinsicWidth(),
-                            drawable.getIntrinsicHeight(),
-                            Bitmap.Config.ARGB_8888);
+                        drawable.getIntrinsicWidth(),
+                        drawable.getIntrinsicHeight(),
+                        Bitmap.Config.ARGB_8888);
+            }
+
+            Canvas canvas = new Canvas(bitmap);
+            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+            drawable.draw(canvas);
         }
-        Canvas canvas = new Canvas(bitmap);
-        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
-        drawable.draw(canvas);
-        if (iconSize > 0 && bitmap.getWidth() > iconSize * 2 || bitmap.getHeight() > iconSize * 2) {
+        if (iconSize <= 0) {
+            return bitmap;
+        }
+
+        if (bitmap.getWidth() < iconSize || bitmap.getHeight() < iconSize
+                || bitmap.getWidth() > iconSize * 2 || bitmap.getHeight() > iconSize * 2) {
             var scaledBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, true);
             if (scaledBitmap != bitmap) {
                 bitmap.recycle();
@@ -235,7 +240,7 @@
     }
 
     @DataClass.Generated(
-            time = 1698789991876L,
+            time = 1705615445673L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/ArchivedActivityInfo.java",
             inputSignatures = "private @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate @android.annotation.NonNull android.content.ComponentName mComponentName\nprivate @android.annotation.Nullable android.graphics.drawable.Drawable mIcon\nprivate @android.annotation.Nullable android.graphics.drawable.Drawable mMonochromeIcon\n @android.annotation.NonNull android.content.pm.ArchivedActivityParcel getParcel()\npublic static  android.graphics.Bitmap drawableToBitmap(android.graphics.drawable.Drawable)\npublic static  android.graphics.Bitmap drawableToBitmap(android.graphics.drawable.Drawable,int)\npublic static  byte[] bytesFromBitmap(android.graphics.Bitmap)\nprivate static  android.graphics.drawable.Drawable drawableFromCompressedBitmap(byte[])\nclass ArchivedActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genBuilder=false, genConstructor=false, genSetters=true)")
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index f0efed9..c4bf18d 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -738,7 +738,7 @@
 
     /**
      * The set of error types that can be set for
-     * {@link #reportUnarchivalStatus(int, int, PendingIntent)}.
+     * {@link #reportUnarchivalState}.
      *
      * @hide
      */
@@ -2421,6 +2421,7 @@
      *                             facilitate the unarchival flow (e.g. user needs to log in).
      * @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists
      */
+    // TODO(b/314960798) Remove old API once it's unused
     @RequiresPermission(anyOf = {
             Manifest.permission.INSTALL_PACKAGES,
             Manifest.permission.REQUEST_INSTALL_PACKAGES})
@@ -2438,6 +2439,30 @@
         }
     }
 
+    /**
+     * Reports the state of an unarchival to the system.
+     *
+     * @see UnarchivalState for the different state options.
+     * @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists
+     */
+    @RequiresPermission(anyOf = {
+            Manifest.permission.INSTALL_PACKAGES,
+            Manifest.permission.REQUEST_INSTALL_PACKAGES})
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public void reportUnarchivalState(@NonNull UnarchivalState unarchivalState)
+            throws PackageManager.NameNotFoundException {
+        Objects.requireNonNull(unarchivalState);
+        try {
+            mInstaller.reportUnarchivalStatus(unarchivalState.getUnarchiveId(),
+                    unarchivalState.getStatus(), unarchivalState.getRequiredStorageBytes(),
+                    unarchivalState.getUserActionIntent(), new UserHandle(mUserId));
+        } catch (ParcelableException e) {
+            e.maybeRethrow(PackageManager.NameNotFoundException.class);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     // (b/239722738) This class serves as a bridge between the PackageLite class, which
     // is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java)
     // This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or
@@ -2693,6 +2718,8 @@
         /** @hide */
         public long rollbackLifetimeMillis = 0;
         /** {@hide} */
+        public int rollbackImpactLevel = PackageManager.ROLLBACK_USER_IMPACT_LOW;
+        /** {@hide} */
         public boolean forceQueryableOverride;
         /** {@hide} */
         public int requireUserAction = USER_ACTION_UNSPECIFIED;
@@ -2749,6 +2776,7 @@
             }
             rollbackDataPolicy = source.readInt();
             rollbackLifetimeMillis = source.readLong();
+            rollbackImpactLevel = source.readInt();
             requireUserAction = source.readInt();
             packageSource = source.readInt();
             applicationEnabledSettingPersistent = source.readBoolean();
@@ -2783,6 +2811,7 @@
             ret.dataLoaderParams = dataLoaderParams;
             ret.rollbackDataPolicy = rollbackDataPolicy;
             ret.rollbackLifetimeMillis = rollbackLifetimeMillis;
+            ret.rollbackImpactLevel = rollbackImpactLevel;
             ret.requireUserAction = requireUserAction;
             ret.packageSource = packageSource;
             ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent;
@@ -3121,6 +3150,28 @@
         }
 
         /**
+         * rollbackImpactLevel is a measure of impact a rollback has on the user. This can take one
+         * of 3 values:
+         * <ul>
+         *     <li>{@link PackageManager#ROLLBACK_USER_IMPACT_LOW} (default)</li>
+         *     <li>{@link PackageManager#ROLLBACK_USER_IMPACT_HIGH} (1)</li>
+         *     <li>{@link PackageManager#ROLLBACK_USER_IMPACT_ONLY_MANUAL} (2)</li>
+         * </ul>
+         *
+         * @hide
+         */
+        @SystemApi
+        @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS)
+        @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION)
+        public void setRollbackImpactLevel(@PackageManager.RollbackImpactLevel int impactLevel) {
+            if ((installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
+                throw new IllegalArgumentException(
+                        "Can't set rollbackImpactLevel when rollback is not enabled");
+            }
+            rollbackImpactLevel = impactLevel;
+        }
+
+        /**
          * @deprecated use {@link #setRequestDowngrade(boolean)}.
          * {@hide}
          */
@@ -3493,6 +3544,7 @@
             pw.printPair("dataLoaderParams", dataLoaderParams);
             pw.printPair("rollbackDataPolicy", rollbackDataPolicy);
             pw.printPair("rollbackLifetimeMillis", rollbackLifetimeMillis);
+            pw.printPair("rollbackImpactLevel", rollbackImpactLevel);
             pw.printPair("applicationEnabledSettingPersistent",
                     applicationEnabledSettingPersistent);
             pw.printHexPair("developmentInstallFlags", developmentInstallFlags);
@@ -3536,6 +3588,7 @@
             }
             dest.writeInt(rollbackDataPolicy);
             dest.writeLong(rollbackLifetimeMillis);
+            dest.writeInt(rollbackImpactLevel);
             dest.writeInt(requireUserAction);
             dest.writeInt(packageSource);
             dest.writeBoolean(applicationEnabledSettingPersistent);
@@ -3734,6 +3787,9 @@
         public long rollbackLifetimeMillis;
 
         /** {@hide} */
+        public int rollbackImpactLevel;
+
+        /** {@hide} */
         public int requireUserAction;
 
         /** {@hide} */
@@ -3801,6 +3857,7 @@
             isPreapprovalRequested = source.readBoolean();
             rollbackDataPolicy = source.readInt();
             rollbackLifetimeMillis = source.readLong();
+            rollbackImpactLevel = source.readInt();
             createdMillis = source.readLong();
             requireUserAction = source.readInt();
             installerUid = source.readInt();
@@ -4438,6 +4495,7 @@
             dest.writeBoolean(isPreapprovalRequested);
             dest.writeInt(rollbackDataPolicy);
             dest.writeLong(rollbackLifetimeMillis);
+            dest.writeInt(rollbackImpactLevel);
             dest.writeLong(createdMillis);
             dest.writeInt(requireUserAction);
             dest.writeInt(installerUid);
@@ -4708,10 +4766,10 @@
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
                 inputSignatures = "private final @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate final @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.annotation.NonNull java.lang.String mPackageName\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<android.content.pm.PackageInstaller.PreapprovalDetails> CREATOR\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\npublic @java.lang.Override int describeContents()\nclass PreapprovalDetails extends java.lang.Object implements [android.os.Parcelable]\nprivate @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate @android.annotation.NonNull java.lang.String mPackageName\nprivate  long mBuilderFieldsSet\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setIcon(android.graphics.Bitmap)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLabel(java.lang.CharSequence)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLocale(android.icu.util.ULocale)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setPackageName(java.lang.String)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails build()\nprivate  void checkNotUsed()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true)")
+
         @Deprecated
         private void __metadata() {}
 
-
         //@formatter:on
         // End of generated code
 
@@ -5102,13 +5160,188 @@
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
                 inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final  boolean mDeviceIdleRequired\nprivate final  boolean mAppNotForegroundRequired\nprivate final  boolean mAppNotInteractingRequired\nprivate final  boolean mAppNotTopVisibleRequired\nprivate final  boolean mNotInCallRequired\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate  boolean mDeviceIdleRequired\nprivate  boolean mAppNotForegroundRequired\nprivate  boolean mAppNotInteractingRequired\nprivate  boolean mAppNotTopVisibleRequired\nprivate  boolean mNotInCallRequired\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setDeviceIdleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotForegroundRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotInteractingRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotTopVisibleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setNotInCallRequired()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genEqualsHashCode=true)")
+
         @Deprecated
         private void __metadata() {}
 
-
         //@formatter:on
         // End of generated code
 
     }
 
+    /**
+     * Used to communicate the unarchival state in {@link #reportUnarchivalState}.
+     */
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public static final class UnarchivalState {
+
+        /**
+         * The caller is able to facilitate the unarchival for the given {@code unarchiveId}.
+         *
+         * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE
+         *                    broadcast with EXTRA_UNARCHIVE_ID.
+         */
+        @NonNull
+        public static UnarchivalState createOkState(int unarchiveId) {
+            return new UnarchivalState(unarchiveId, UNARCHIVAL_OK, /* requiredStorageBytes= */ -1,
+                    /* userActionIntent= */ null);
+        }
+
+        /**
+         * User action is required before commencing with the unarchival for the given
+         * {@code unarchiveId}. E.g., this could be used if it's necessary for the user to sign-in
+         * first.
+         *
+         * @param unarchiveId      the ID provided by the system as part of the
+         *                         intent.action.UNARCHIVE
+         *                         broadcast with EXTRA_UNARCHIVE_ID.
+         * @param userActionIntent optional intent to start a follow up action required to
+         *                         facilitate the unarchival flow (e.g. user needs to log in).
+         */
+        @NonNull
+        public static UnarchivalState createUserActionRequiredState(int unarchiveId,
+                @NonNull PendingIntent userActionIntent) {
+            Objects.requireNonNull(userActionIntent);
+            return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_USER_ACTION_NEEDED,
+                    /* requiredStorageBytes= */ -1, userActionIntent);
+        }
+
+        /**
+         * There is not enough storage to start the unarchival for the given {@code unarchiveId}.
+         *
+         * @param unarchiveId          the ID provided by the system as part of the
+         *                             intent.action.UNARCHIVE
+         *                             broadcast with EXTRA_UNARCHIVE_ID.
+         * @param requiredStorageBytes ff the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this
+         *                             field should be set to specify how many additional bytes of
+         *                             storage are required to unarchive the app.
+         * @param userActionIntent     can optionally be set to provide a custom storage-clearing
+         *                             action.
+         */
+        @NonNull
+        public static UnarchivalState createInsufficientStorageState(int unarchiveId,
+                long requiredStorageBytes, @Nullable PendingIntent userActionIntent) {
+            return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE,
+                    requiredStorageBytes, userActionIntent);
+        }
+
+        /**
+         * The device has no data connectivity and unarchival cannot be started for the given
+         * {@code unarchiveId}.
+         *
+         * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE
+         *                    broadcast with EXTRA_UNARCHIVE_ID.
+         */
+        @NonNull
+        public static UnarchivalState createNoConnectivityState(int unarchiveId) {
+            return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_NO_CONNECTIVITY,
+                    /* requiredStorageBytes= */ -1,/* userActionIntent= */ null);
+        }
+
+        /**
+         * Generic error state for all cases that are not covered by other methods in this class.
+         *
+         * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE
+         *                    broadcast with EXTRA_UNARCHIVE_ID.
+         */
+        @NonNull
+        public static UnarchivalState createGenericErrorState(int unarchiveId) {
+            return new UnarchivalState(unarchiveId, UNARCHIVAL_GENERIC_ERROR,
+                    /* requiredStorageBytes= */ -1,/* userActionIntent= */ null);
+        }
+
+
+        /**
+         * The ID provided by the system as part of the intent.action.UNARCHIVE broadcast with
+         * EXTRA_UNARCHIVE_ID.
+         */
+        private final int mUnarchiveId;
+
+        /** Used for the system to provide the user with necessary follow-up steps or errors. */
+        @UnarchivalStatus
+        private final int mStatus;
+
+        /**
+         * If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field should be set to specify
+         * how many additional bytes of storage are required to unarchive the app.
+         */
+        private final long mRequiredStorageBytes;
+
+        /**
+         * Optional intent to start a follow up action required to facilitate the unarchival flow
+         * (e.g., user needs to log in).
+         */
+        @Nullable
+        private final PendingIntent mUserActionIntent;
+
+        /**
+         * Creates a new UnarchivalState.
+         *
+         * @param unarchiveId          The ID provided by the system as part of the
+         *                             intent.action.UNARCHIVE broadcast with
+         *                             EXTRA_UNARCHIVE_ID.
+         * @param status               Used for the system to provide the user with necessary
+         *                             follow-up steps or errors.
+         * @param requiredStorageBytes If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this
+         *                             field should be set to specify
+         *                             how many additional bytes of storage are required to
+         *                             unarchive the app.
+         * @param userActionIntent     Optional intent to start a follow up action required to
+         *                             facilitate the unarchival flow
+         *                             (e.g,. user needs to log in).
+         * @hide
+         */
+        private UnarchivalState(
+                int unarchiveId,
+                @UnarchivalStatus int status,
+                long requiredStorageBytes,
+                @Nullable PendingIntent userActionIntent) {
+            this.mUnarchiveId = unarchiveId;
+            this.mStatus = status;
+            com.android.internal.util.AnnotationValidations.validate(
+                    UnarchivalStatus.class, null, mStatus);
+            this.mRequiredStorageBytes = requiredStorageBytes;
+            this.mUserActionIntent = userActionIntent;
+        }
+
+        /**
+         * The ID provided by the system as part of the intent.action.UNARCHIVE broadcast with
+         * EXTRA_UNARCHIVE_ID.
+         *
+         * @hide
+         */
+        int getUnarchiveId() {
+            return mUnarchiveId;
+        }
+
+        /**
+         * Used for the system to provide the user with necessary follow-up steps or errors.
+         *
+         * @hide
+         */
+        @UnarchivalStatus int getStatus() {
+            return mStatus;
+        }
+
+        /**
+         * If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field should be set to specify
+         * how many additional bytes of storage are required to unarchive the app.
+         *
+         * @hide
+         */
+        long getRequiredStorageBytes() {
+            return mRequiredStorageBytes;
+        }
+
+        /**
+         * Optional intent to start a follow up action required to facilitate the unarchival flow
+         * (e.g. user needs to log in).
+         *
+         * @hide
+         */
+        @Nullable PendingIntent getUserActionIntent() {
+            return mUserActionIntent;
+        }
+    }
+
 }
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index aabbe69..4724e86 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1501,6 +1501,44 @@
     public static final int ROLLBACK_DATA_POLICY_RETAIN = 2;
 
     /** @hide */
+    @IntDef(prefix = {"ROLLBACK_USER_IMPACT_"}, value = {
+            ROLLBACK_USER_IMPACT_LOW,
+            ROLLBACK_USER_IMPACT_HIGH,
+            ROLLBACK_USER_IMPACT_ONLY_MANUAL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RollbackImpactLevel {}
+
+    /**
+     * Rollback will be performed automatically in response to native crashes on startup or
+     * persistent service crashes. More suitable for apps that do not store any user data.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION)
+    public static final int ROLLBACK_USER_IMPACT_LOW = 0;
+
+    /**
+     * Rollback will be performed automatically only when the device is found to be unrecoverable.
+     * More suitable for apps that store user data and have higher impact on user.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION)
+    public static final int ROLLBACK_USER_IMPACT_HIGH = 1;
+
+    /**
+     * Rollback will not be performed automatically. It can be triggered externally.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION)
+    public static final int ROLLBACK_USER_IMPACT_ONLY_MANUAL = 2;
+
+    /** @hide */
     @IntDef(flag = true, prefix = { "INSTALL_" }, value = {
             INSTALL_REPLACE_EXISTING,
             INSTALL_ALLOW_TEST,
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index a2cd3e1..e4e9fba 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -146,3 +146,10 @@
     bug: "281848623"
 }
 
+flag {
+    name: "recoverability_detection"
+    namespace: "package_manager_service"
+    description: "Feature flag to enable recoverability detection feature. It includes GMS core rollback and improvements to rescue party."
+    bug: "291135724"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 7c5d305..5bfc012 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -77,4 +77,11 @@
     namespace: "profile_experiences"
     description: "Move the quiet mode operations, happening on a background thread today, to a separate thread."
     bug: "320483504"
+}
+
+flag {
+    name: "enable_private_space_autolock_on_restarts"
+    namespace: "profile_experiences"
+    description: "Enable auto-locking private space on device restarts"
+    bug: "296993385"
 }
\ No newline at end of file
diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java
index a4db733..bd74b0b 100644
--- a/core/java/android/content/pm/overlay/OverlayPaths.java
+++ b/core/java/android/content/pm/overlay/OverlayPaths.java
@@ -49,6 +49,13 @@
     public static class Builder {
         final OverlayPaths mPaths = new OverlayPaths();
 
+        public Builder() {}
+
+        public Builder(@NonNull OverlayPaths base) {
+            mPaths.mResourceDirs.addAll(base.getResourceDirs());
+            mPaths.mOverlayPaths.addAll(base.getOverlayPaths());
+        }
+
         /**
          * Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}.
          */
diff --git a/core/java/android/content/rollback/RollbackInfo.java b/core/java/android/content/rollback/RollbackInfo.java
index a363718..d128055 100644
--- a/core/java/android/content/rollback/RollbackInfo.java
+++ b/core/java/android/content/rollback/RollbackInfo.java
@@ -16,8 +16,12 @@
 
 package android.content.rollback;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.pm.Flags;
+import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -25,17 +29,14 @@
 import java.util.List;
 
 /**
- * Information about a set of packages that can be, or already have been
- * rolled back together.
+ * Information about a set of packages that can be, or already have been rolled back together.
  *
  * @hide
  */
 @SystemApi
 public final class RollbackInfo implements Parcelable {
 
-    /**
-     * A unique identifier for the rollback.
-     */
+    /** A unique identifier for the rollback. */
     private final int mRollbackId;
 
     private final List<PackageRollbackInfo> mPackages;
@@ -44,15 +45,39 @@
 
     private final boolean mIsStaged;
     private int mCommittedSessionId;
+    private int mRollbackImpactLevel;
 
     /** @hide */
-    public RollbackInfo(int rollbackId, List<PackageRollbackInfo> packages, boolean isStaged,
-            List<VersionedPackage> causePackages,  int committedSessionId) {
+    public RollbackInfo(
+            int rollbackId,
+            List<PackageRollbackInfo> packages,
+            boolean isStaged,
+            List<VersionedPackage> causePackages,
+            int committedSessionId,
+            @PackageManager.RollbackImpactLevel int rollbackImpactLevel) {
         this.mRollbackId = rollbackId;
         this.mPackages = packages;
         this.mIsStaged = isStaged;
         this.mCausePackages = causePackages;
         this.mCommittedSessionId = committedSessionId;
+        this.mRollbackImpactLevel = rollbackImpactLevel;
+    }
+
+    /** @hide */
+    public RollbackInfo(
+            int rollbackId,
+            List<PackageRollbackInfo> packages,
+            boolean isStaged,
+            List<VersionedPackage> causePackages,
+            int committedSessionId) {
+        // If impact level is not set default to 0
+        this(
+                rollbackId,
+                packages,
+                isStaged,
+                causePackages,
+                committedSessionId,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
     }
 
     private RollbackInfo(Parcel in) {
@@ -61,34 +86,28 @@
         mIsStaged = in.readBoolean();
         mCausePackages = in.createTypedArrayList(VersionedPackage.CREATOR);
         mCommittedSessionId = in.readInt();
+        mRollbackImpactLevel = in.readInt();
     }
 
-    /**
-     * Returns a unique identifier for this rollback.
-     */
+    /** Returns a unique identifier for this rollback. */
     public int getRollbackId() {
         return mRollbackId;
     }
 
-    /**
-     * Returns the list of package that are rolled back.
-     */
+    /** Returns the list of package that are rolled back. */
     @NonNull
     public List<PackageRollbackInfo> getPackages() {
         return mPackages;
     }
 
-    /**
-     * Returns true if this rollback requires reboot to take effect after
-     * being committed.
-     */
+    /** Returns true if this rollback requires reboot to take effect after being committed. */
     public boolean isStaged() {
         return mIsStaged;
     }
 
     /**
-     * Returns the session ID for the committed rollback for staged rollbacks.
-     * Only applicable for rollbacks that have been committed.
+     * Returns the session ID for the committed rollback for staged rollbacks. Only applicable for
+     * rollbacks that have been committed.
      */
     public int getCommittedSessionId() {
         return mCommittedSessionId;
@@ -96,6 +115,7 @@
 
     /**
      * Sets the session ID for the committed rollback for staged rollbacks.
+     *
      * @hide
      */
     public void setCommittedSessionId(int sessionId) {
@@ -103,15 +123,40 @@
     }
 
     /**
-     * Gets the list of package versions that motivated this rollback.
-     * As provided to {@link #commitRollback} when the rollback was committed.
-     * This is only applicable for rollbacks that have been committed.
+     * Gets the list of package versions that motivated this rollback. As provided to {@link
+     * #commitRollback} when the rollback was committed. This is only applicable for rollbacks that
+     * have been committed.
      */
     @NonNull
     public List<VersionedPackage> getCausePackages() {
         return mCausePackages;
     }
 
+    /**
+     * Get rollback impact level. Refer {@link
+     * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info
+     * on impact level.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION)
+    public @PackageManager.RollbackImpactLevel int getRollbackImpactLevel() {
+        return mRollbackImpactLevel;
+    }
+
+    /**
+     * Set rollback impact level. Refer {@link
+     * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info
+     * on impact level.
+     *
+     * @hide
+     */
+    public void setRollbackImpactLevel(
+            @PackageManager.RollbackImpactLevel int rollbackImpactLevel) {
+        mRollbackImpactLevel = rollbackImpactLevel;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -124,16 +169,17 @@
         out.writeBoolean(mIsStaged);
         out.writeTypedList(mCausePackages);
         out.writeInt(mCommittedSessionId);
+        out.writeInt(mRollbackImpactLevel);
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<RollbackInfo> CREATOR =
             new Parcelable.Creator<RollbackInfo>() {
-        public RollbackInfo createFromParcel(Parcel in) {
-            return new RollbackInfo(in);
-        }
+                public RollbackInfo createFromParcel(Parcel in) {
+                    return new RollbackInfo(in);
+                }
 
-        public RollbackInfo[] newArray(int size) {
-            return new RollbackInfo[size];
-        }
-    };
+                public RollbackInfo[] newArray(int size) {
+                    return new RollbackInfo[size];
+                }
+            };
 }
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 8a4f678..35ae3c9 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -40,12 +40,15 @@
 import android.os.OperationCanceledException;
 import android.os.SystemProperties;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Pair;
 import android.util.Printer;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
 import dalvik.annotation.optimization.NeverCompile;
@@ -103,8 +106,14 @@
     // Stores reference to all databases opened in the current process.
     // (The referent Object is not used at this time.)
     // INVARIANT: Guarded by sActiveDatabases.
+    @GuardedBy("sActiveDatabases")
     private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases = new WeakHashMap<>();
 
+    // Tracks which database files are currently open.  If a database file is opened more than
+    // once at any given moment, the associated databases are marked as "concurrent".
+    @GuardedBy("sActiveDatabases")
+    private static final OpenTracker sOpenTracker = new OpenTracker();
+
     // Thread-local for database sessions that belong to this database.
     // Each thread has its own database session.
     // INVARIANT: Immutable.
@@ -510,6 +519,7 @@
 
     private void dispose(boolean finalized) {
         final SQLiteConnectionPool pool;
+        final String path;
         synchronized (mLock) {
             if (mCloseGuardLocked != null) {
                 if (finalized) {
@@ -520,10 +530,12 @@
 
             pool = mConnectionPoolLocked;
             mConnectionPoolLocked = null;
+            path = isInMemoryDatabase() ? null : getPath();
         }
 
         if (!finalized) {
             synchronized (sActiveDatabases) {
+                sOpenTracker.close(path);
                 sActiveDatabases.remove(this);
             }
 
@@ -1132,6 +1144,74 @@
         }
     }
 
+    /**
+     * Track the number of times a database file has been opened.  There is a primary connection
+     * associated with every open database, and these can contend with each other, leading to
+     * unexpected SQLiteDatabaseLockedException exceptions.  The tracking here is only advisory:
+     * multiply-opened databases are logged but no other action is taken.
+     *
+     * This class is not thread-safe.
+     */
+    private static class OpenTracker {
+        // The list of currently-open databases.  This maps the database file to the number of
+        // currently-active opens.
+        private final ArrayMap<String, Integer> mOpens = new ArrayMap<>();
+
+        // The maximum number of concurrently open database paths that will be stored.  Once this
+        // many paths have been recorded, further paths are logged but not saved.
+        private static final int MAX_RECORDED_PATHS = 20;
+
+        // The list of databases that were ever concurrently opened.
+        private final ArraySet<String> mConcurrent = new ArraySet<>();
+
+        /** Return the canonical path.  On error, just return the input path. */
+        private static String normalize(String path) {
+            try {
+                return new File(path).toPath().toRealPath().toString();
+            } catch (Exception e) {
+                // If there is an IO or security exception, just continue, using the input path.
+                return path;
+            }
+        }
+
+        /** Return true if the path is currently open in another SQLiteDatabase instance. */
+        void open(@Nullable String path) {
+            if (path == null) return;
+            path = normalize(path);
+
+            Integer count = mOpens.get(path);
+            if (count == null || count == 0) {
+                mOpens.put(path, 1);
+                return;
+            } else {
+                mOpens.put(path, count + 1);
+                if (mConcurrent.size() < MAX_RECORDED_PATHS) {
+                    mConcurrent.add(path);
+                }
+                Log.w(TAG, "multiple primary connections on " + path);
+                return;
+            }
+        }
+
+        void close(@Nullable String path) {
+            if (path == null) return;
+            path = normalize(path);
+            Integer count = mOpens.get(path);
+            if (count == null || count <= 0) {
+                Log.e(TAG, "open database counting failure on " + path);
+            } else if (count == 1) {
+                // Implicitly set the count to zero, and make mOpens smaller.
+                mOpens.remove(path);
+            } else {
+                mOpens.put(path, count - 1);
+            }
+        }
+
+        ArraySet<String> getConcurrentDatabasePaths() {
+            return new ArraySet<>(mConcurrent);
+        }
+    }
+
     private void open() {
         try {
             try {
@@ -1153,14 +1233,17 @@
     }
 
     private void openInner() {
+        final String path;
         synchronized (mLock) {
             assert mConnectionPoolLocked == null;
             mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked);
             mCloseGuardLocked.open("close");
+            path = isInMemoryDatabase() ? null : getPath();
         }
 
         synchronized (sActiveDatabases) {
             sActiveDatabases.put(this, null);
+            sOpenTracker.open(path);
         }
     }
 
@@ -2345,6 +2428,17 @@
     }
 
     /**
+     * Return list of databases that have been concurrently opened.
+     * @hide
+     */
+    @VisibleForTesting
+    public static ArraySet<String> getConcurrentDatabasePaths() {
+        synchronized (sActiveDatabases) {
+            return sOpenTracker.getConcurrentDatabasePaths();
+        }
+    }
+
+    /**
      * Returns true if the new version code is greater than the current database version.
      *
      * @param newVersion The new version code.
@@ -2766,6 +2860,19 @@
                 dumpDatabaseDirectory(printer, new File(dir), isSystem);
             }
         }
+
+        // Dump concurrently-opened database files, if any
+        final ArraySet<String> concurrent;
+        synchronized (sActiveDatabases) {
+            concurrent = sOpenTracker.getConcurrentDatabasePaths();
+        }
+        if (concurrent.size() > 0) {
+            printer.println("");
+            printer.println("Concurrently opened database files");
+            for (String f : concurrent) {
+                printer.println("  " + f);
+            }
+        }
     }
 
     private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) {
diff --git a/core/java/android/os/VintfObject.java b/core/java/android/os/VintfObject.java
index 4fc5131..5056557 100644
--- a/core/java/android/os/VintfObject.java
+++ b/core/java/android/os/VintfObject.java
@@ -31,6 +31,10 @@
 
     private static final String LOG_TAG = "VintfObject";
 
+    static {
+        System.loadLibrary("vintf_jni");
+    }
+
     /**
      * Slurps all device information (both manifests and both matrices)
      * and report them.
diff --git a/core/java/android/os/VintfRuntimeInfo.java b/core/java/android/os/VintfRuntimeInfo.java
index f17039b..e729063 100644
--- a/core/java/android/os/VintfRuntimeInfo.java
+++ b/core/java/android/os/VintfRuntimeInfo.java
@@ -28,6 +28,10 @@
 
     private VintfRuntimeInfo() {}
 
+    static {
+        System.loadLibrary("vintf_jni");
+    }
+
     /**
      * @return /sys/fs/selinux/policyvers, via security_policyvers() native call
      *
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 39b6aeb..8c70501 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -86,3 +86,11 @@
     description: "This flag is used to enabled the Wallet Role for all users on the device"
     bug: "283989236"
 }
+
+flag {
+  name: "signature_permission_allowlist_enabled"
+  is_fixed_read_only: true
+  namespace: "permissions"
+  description: "Enable signature permission allowlist"
+  bug: "308573169"
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ab98c94..d946430 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3243,9 +3243,17 @@
 
         private static final String NAME_EQ_PLACEHOLDER = "name=?";
 
+        // Cached values of queried settings.
+        // Key is the setting's name, value is the setting's value.
         // Must synchronize on 'this' to access mValues and mValuesVersion.
         private final ArrayMap<String, String> mValues = new ArrayMap<>();
 
+        // Cached values for queried prefixes.
+        // Key is the prefix, value is all of the settings under the prefix, mapped from a setting's
+        // name to a setting's value. The name string doesn't include the prefix.
+        // Must synchronize on 'this' to access.
+        private final ArrayMap<String, ArrayMap<String, String>> mPrefixToValues = new ArrayMap<>();
+
         private final Uri mUri;
         @UnsupportedAppUsage
         private final ContentProviderHolder mProviderHolder;
@@ -3592,15 +3600,13 @@
                     || applicationInfo.isSignedWithPlatformKey();
         }
 
-        private ArrayMap<String, String> getStringsForPrefixStripPrefix(
-                ContentResolver cr, String prefix, String[] names) {
+        private Map<String, String> getStringsForPrefixStripPrefix(
+                ContentResolver cr, String prefix, List<String> names) {
             String namespace = prefix.substring(0, prefix.length() - 1);
             ArrayMap<String, String> keyValues = new ArrayMap<>();
             int substringLength = prefix.length();
-
             int currentGeneration = -1;
             boolean needsGenerationTracker = false;
-
             synchronized (NameValueCache.this) {
                 final GenerationTracker generationTracker = mGenerationTrackers.get(prefix);
                 if (generationTracker != null) {
@@ -3614,40 +3620,24 @@
                         // generation tracker and request a new one
                         generationTracker.destroy();
                         mGenerationTrackers.remove(prefix);
-                        for (int i = mValues.size() - 1; i >= 0; i--) {
-                            String key = mValues.keyAt(i);
-                            if (key.startsWith(prefix)) {
-                                mValues.remove(key);
-                            }
-                        }
+                        mPrefixToValues.remove(prefix);
                         needsGenerationTracker = true;
                     } else {
-                        boolean prefixCached = mValues.containsKey(prefix);
-                        if (prefixCached) {
-                            if (DEBUG) {
-                                Log.i(TAG, "Cache hit for prefix:" + prefix);
-                            }
-                            if (names.length > 0) {
+                        final ArrayMap<String, String> cachedSettings = mPrefixToValues.get(prefix);
+                        if (cachedSettings != null) {
+                            if (!names.isEmpty()) {
                                 for (String name : names) {
-                                    // mValues can contain "null" values, need to use containsKey.
-                                    if (mValues.containsKey(name)) {
+                                    // The cache can contain "null" values, need to use containsKey.
+                                    if (cachedSettings.containsKey(name)) {
                                         keyValues.put(
-                                                name.substring(substringLength),
-                                                mValues.get(name));
+                                                name,
+                                                cachedSettings.get(name));
                                     }
                                 }
                             } else {
-                                for (int i = 0; i < mValues.size(); ++i) {
-                                    String key = mValues.keyAt(i);
-                                    // Explicitly exclude the prefix as it is only there to
-                                    // signal that the prefix has been cached.
-                                    if (key.startsWith(prefix) && !key.equals(prefix)) {
-                                        String value = mValues.valueAt(i);
-                                        keyValues.put(
-                                                key.substring(substringLength),
-                                                value);
-                                    }
-                                }
+                                keyValues.putAll(cachedSettings);
+                                // Remove the hack added for the legacy behavior.
+                                keyValues.remove("");
                             }
                             return keyValues;
                         }
@@ -3657,7 +3647,6 @@
                     needsGenerationTracker = true;
                 }
             }
-
             if (mCallListCommand == null) {
                 // No list command specified, return empty map
                 return keyValues;
@@ -3702,20 +3691,23 @@
                 }
 
                 // All flags for the namespace
-                Map<String, String> flagsToValues =
+                HashMap<String, String> flagsToValues =
                         (HashMap) b.getSerializable(Settings.NameValueTable.VALUE, java.util.HashMap.class);
+                if (flagsToValues == null) {
+                    return keyValues;
+                }
                 // Only the flags requested by the caller
-                if (names.length > 0) {
+                if (!names.isEmpty()) {
                     for (String name : names) {
                         // flagsToValues can contain "null" values, need to use containsKey.
-                        if (flagsToValues.containsKey(name)) {
+                        final String key = Config.createCompositeName(namespace, name);
+                        if (flagsToValues.containsKey(key)) {
                             keyValues.put(
-                                    name.substring(substringLength),
-                                    flagsToValues.get(name));
+                                    name,
+                                    flagsToValues.get(key));
                         }
                     }
                 } else {
-                    keyValues.ensureCapacity(keyValues.size() + flagsToValues.size());
                     for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
                         keyValues.put(
                                 flag.getKey().substring(substringLength),
@@ -3751,10 +3743,18 @@
                         if (DEBUG) {
                             Log.i(TAG, "Updating cache for prefix:" + prefix);
                         }
-                        // cache the complete list of flags for the namespace
-                        mValues.putAll(flagsToValues);
-                        // Adding the prefix as a signal that the prefix is cached.
-                        mValues.put(prefix, null);
+                        // Cache the complete list of flags for the namespace for bulk queries.
+                        // In this cached list, the setting's name doesn't include the prefix.
+                        ArrayMap<String, String> namesToValues =
+                                new ArrayMap<>(flagsToValues.size() + 1);
+                        for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
+                            namesToValues.put(
+                                    flag.getKey().substring(substringLength),
+                                    flag.getValue());
+                        }
+                        // Legacy behavior, we return <"", null> when queried with name = ""
+                        namesToValues.put("", null);
+                        mPrefixToValues.put(prefix, namesToValues);
                     }
                 }
                 return keyValues;
@@ -19945,16 +19945,9 @@
         @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
         public static Map<String, String> getStrings(@NonNull ContentResolver resolver,
                 @NonNull String namespace, @NonNull List<String> names) {
-            String[] compositeNames = new String[names.size()];
-            for (int i = 0, size = names.size(); i < size; ++i) {
-                compositeNames[i] = createCompositeName(namespace, names.get(i));
-            }
-
             String prefix = createPrefix(namespace);
 
-            ArrayMap<String, String> keyValues = sNameValueCache.getStringsForPrefixStripPrefix(
-                    resolver, prefix, compositeNames);
-            return keyValues;
+            return sNameValueCache.getStringsForPrefixStripPrefix(resolver, prefix, names);
         }
 
         /**
@@ -20276,7 +20269,7 @@
             }
         }
 
-        private static String createCompositeName(@NonNull String namespace, @NonNull String name) {
+        static String createCompositeName(@NonNull String namespace, @NonNull String name) {
             Preconditions.checkNotNull(namespace);
             Preconditions.checkNotNull(name);
             var sb = new StringBuilder(namespace.length() + 1 + name.length());
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index c479877..9895551 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -185,7 +185,13 @@
             SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
                     | SUPPRESSED_EFFECT_LIGHTS | SUPPRESSED_EFFECT_PEEK | SUPPRESSED_EFFECT_AMBIENT;
 
-    public static final int XML_VERSION = 8;
+    // ZenModeConfig XML versions distinguishing key changes.
+    public static final int XML_VERSION_ZEN_UPGRADE = 8;
+    public static final int XML_VERSION_MODES_API = 11;
+
+    // TODO: b/310620812 - Update XML_VERSION and update default_zen_config.xml accordingly when
+    //       modes_api is inlined.
+    private static final int XML_VERSION = 10;
     public static final String ZEN_TAG = "zen";
     private static final String ZEN_ATT_VERSION = "version";
     private static final String ZEN_ATT_USER = "user";
@@ -586,6 +592,10 @@
         }
     }
 
+    public static int getCurrentXmlVersion() {
+        return Flags.modesApi() ? XML_VERSION_MODES_API : XML_VERSION;
+    }
+
     public static ZenModeConfig readXml(TypedXmlPullParser parser)
             throws XmlPullParserException, IOException {
         int type = parser.getEventType();
@@ -593,7 +603,7 @@
         String tag = parser.getName();
         if (!ZEN_TAG.equals(tag)) return null;
         final ZenModeConfig rt = new ZenModeConfig();
-        rt.version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION);
+        rt.version = safeInt(parser, ZEN_ATT_VERSION, getCurrentXmlVersion());
         rt.user = safeInt(parser, ZEN_ATT_USER, rt.user);
         boolean readSuppressedEffects = false;
         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
@@ -707,14 +717,16 @@
     /**
      * Writes XML of current ZenModeConfig
      * @param out serializer
-     * @param version uses XML_VERSION if version is null
+     * @param version uses the current XML version if version is null
      * @throws IOException
      */
+
     public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup)
             throws IOException {
+        int xmlVersion = getCurrentXmlVersion();
         out.startTag(null, ZEN_TAG);
         out.attribute(null, ZEN_ATT_VERSION, version == null
-                ? Integer.toString(XML_VERSION) : Integer.toString(version));
+                ? Integer.toString(xmlVersion) : Integer.toString(version));
         out.attributeInt(null, ZEN_ATT_USER, user);
         out.startTag(null, ALLOW_TAG);
         out.attributeBoolean(null, ALLOW_ATT_CALLS, allowCalls);
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/core/java/android/service/notification/ZenPolicy.aidl
similarity index 90%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to core/java/android/service/notification/ZenPolicy.aidl
index 33143fe..b56f5c6 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/core/java/android/service/notification/ZenPolicy.aidl
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package android.service.notification;
 
-parcelable WlcLDeviceInfo;
+parcelable ZenPolicy;
\ No newline at end of file
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index fb491d0..d8318a6 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -1422,6 +1422,54 @@
     }
 
     /**
+     * Overwrites any policy values in this ZenPolicy with set values from newPolicy and
+     * returns a copy of the resulting ZenPolicy.
+     * Unlike apply(), values set in newPolicy will always be kept over pre-existing
+     * fields. Any values in newPolicy that are not set keep their currently set values.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public @NonNull ZenPolicy overwrittenWith(@Nullable ZenPolicy newPolicy) {
+        ZenPolicy result = this.copy();
+
+        if (newPolicy == null) {
+            return result;
+        }
+
+        // set priority categories
+        for (int category = 0; category < mPriorityCategories.size(); category++) {
+            @State int newState = newPolicy.mPriorityCategories.get(category);
+            if (newState != STATE_UNSET) {
+                result.mPriorityCategories.set(category, newState);
+
+                if (category == PRIORITY_CATEGORY_MESSAGES) {
+                    result.mPriorityMessages = newPolicy.mPriorityMessages;
+                } else if (category == PRIORITY_CATEGORY_CALLS) {
+                    result.mPriorityCalls = newPolicy.mPriorityCalls;
+                } else if (category == PRIORITY_CATEGORY_CONVERSATIONS) {
+                    result.mConversationSenders = newPolicy.mConversationSenders;
+                }
+            }
+        }
+
+        // set visual effects
+        for (int visualEffect = 0; visualEffect < mVisualEffects.size(); visualEffect++) {
+            if (newPolicy.mVisualEffects.get(visualEffect) != STATE_UNSET) {
+                result.mVisualEffects.set(visualEffect, newPolicy.mVisualEffects.get(visualEffect));
+            }
+        }
+
+        // set allowed channels
+        if (newPolicy.mAllowChannels != CHANNEL_POLICY_UNSET) {
+            result.mAllowChannels = newPolicy.mAllowChannels;
+        }
+
+        return result;
+    }
+
+    /**
      * @hide
      */
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 1a2be15..76e0c25 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -16,6 +16,7 @@
 
 package android.service.wallpaper;
 
+import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH;
 import static android.app.WallpaperManager.COMMAND_FREEZE;
 import static android.app.WallpaperManager.COMMAND_UNFREEZE;
 import static android.app.WallpaperManager.SetWallpaperFlags;
@@ -153,6 +154,7 @@
     static final boolean DEBUG = false;
     static final float MIN_PAGE_ALLOWED_MARGIN = .05f;
     private static final int MIN_BITMAP_SCREENSHOT_WIDTH = 64;
+    private static final long PRESERVE_VISIBLE_TIMEOUT_MS = 1000;
     private static final long DEFAULT_UPDATE_SCREENSHOT_DURATION = 60 * 1000; //Once per minute
     private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
             new RectF(0, 0, 1, 1);
@@ -165,6 +167,7 @@
 
     private static final int MSG_UPDATE_SURFACE = 10000;
     private static final int MSG_VISIBILITY_CHANGED = 10010;
+    private static final int MSG_REFRESH_VISIBILITY = 10011;
     private static final int MSG_WALLPAPER_OFFSETS = 10020;
     private static final int MSG_WALLPAPER_COMMAND = 10025;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -248,6 +251,11 @@
          */
         private boolean mIsScreenTurningOn;
         boolean mReportedVisible;
+        /**
+         * This is used with {@link #PRESERVE_VISIBLE_TIMEOUT_MS} to avoid intermediate visibility
+         * changes if the display may be toggled in a short time, e.g. display switch.
+         */
+        boolean mPreserveVisible;
         boolean mDestroyed;
         // Set to true after receiving WallpaperManager#COMMAND_FREEZE. It's reset back to false
         // after receiving WallpaperManager#COMMAND_UNFREEZE. COMMAND_FREEZE is fully applied once
@@ -1084,6 +1092,9 @@
             if (pendingCount != 0) {
                 out.print(prefix); out.print("mPendingResizeCount="); out.println(pendingCount);
             }
+            if (mPreserveVisible) {
+                out.print(prefix); out.print("mPreserveVisible=true");
+            }
             synchronized (mLock) {
                 out.print(prefix); out.print("mPendingXOffset="); out.print(mPendingXOffset);
                         out.print(" mPendingXOffset="); out.println(mPendingXOffset);
@@ -1643,7 +1654,8 @@
                                 ? false
                                 : mIWallpaperEngine.mInfo.supportsAmbientMode();
                 // Report visibility only if display is fully on or wallpaper supports ambient mode.
-                boolean visible = mVisible && (displayFullyOn || supportsAmbientMode);
+                final boolean visible = (mVisible && (displayFullyOn || supportsAmbientMode))
+                        || mPreserveVisible;
                 if (DEBUG) {
                     Log.v(
                             TAG,
@@ -2080,6 +2092,9 @@
             if (!mDestroyed) {
                 if (COMMAND_FREEZE.equals(cmd.action) || COMMAND_UNFREEZE.equals(cmd.action)) {
                     updateFrozenState(/* frozenRequested= */ !COMMAND_UNFREEZE.equals(cmd.action));
+                } else if (COMMAND_DISPLAY_SWITCH.equals(cmd.action)) {
+                    handleDisplaySwitch(cmd.z == 1 /* startToSwitch */);
+                    return;
                 }
                 result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z,
                         cmd.extras, cmd.sync);
@@ -2095,6 +2110,23 @@
             }
         }
 
+        private void handleDisplaySwitch(boolean startToSwitch) {
+            if (startToSwitch && mReportedVisible) {
+                // The display may be off/on in a short time when the display is switching.
+                // Keep the visible state until onScreenTurnedOn or !startToSwitch is received, so
+                // the rendering thread can be active to redraw in time when receiving size change.
+                mPreserveVisible = true;
+                mCaller.removeMessages(MSG_REFRESH_VISIBILITY);
+                mCaller.sendMessageDelayed(mCaller.obtainMessage(MSG_REFRESH_VISIBILITY),
+                        PRESERVE_VISIBLE_TIMEOUT_MS);
+            } else if (!startToSwitch && mPreserveVisible) {
+                // The switch is finished, so restore to actual visibility.
+                mPreserveVisible = false;
+                mCaller.removeMessages(MSG_REFRESH_VISIBILITY);
+                reportVisibility(false /* forceReport */);
+            }
+        }
+
         private void updateFrozenState(boolean frozenRequested) {
             if (mIWallpaperEngine.mInfo == null
                     // Procees the unfreeze command in case the wallaper became static while
@@ -2638,6 +2670,10 @@
                             + ": " + message.arg1);
                     mEngine.doVisibilityChanged(message.arg1 != 0);
                     break;
+                case MSG_REFRESH_VISIBILITY:
+                    mEngine.mPreserveVisible = false;
+                    mEngine.reportVisibility(false /* forceReport */);
+                    break;
                 case MSG_UPDATE_SCREEN_TURNING_ON:
                     if (DEBUG) {
                         Log.v(TAG,
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 8935ab3..0a813a3 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -38,6 +38,7 @@
 import android.telephony.Annotation.RadioPowerState;
 import android.telephony.Annotation.SimActivationState;
 import android.telephony.Annotation.SrvccState;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
 import android.telephony.TelephonyManager.CarrierPrivilegesCallback;
 import android.telephony.emergency.EmergencyNumber;
 import android.telephony.ims.ImsCallSession;
@@ -116,14 +117,15 @@
     }
 
     /**
-     * Register for changes to the list of active {@link SubscriptionInfo} records or to the
-     * individual records themselves. When a change occurs the onSubscriptionsChanged method of
-     * the listener will be invoked immediately if there has been a notification. The
-     * onSubscriptionChanged method will also be triggered once initially when calling this
-     * function.
+     * Register for changes to the list of {@link SubscriptionInfo} records or to the
+     * individual records (active or inactive) themselves. When a change occurs, the
+     * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method of
+     * the listener will be invoked immediately. The
+     * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method will also be invoked
+     * once initially when calling this method.
      *
-     * @param listener an instance of {@link SubscriptionManager.OnSubscriptionsChangedListener}
-     *                 with onSubscriptionsChanged overridden.
+     * @param listener an instance of {@link OnSubscriptionsChangedListener} with
+     * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} overridden.
      * @param executor the executor that will execute callbacks.
      * @hide
      */
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index eca848a..1ea80f1 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -152,7 +152,8 @@
     /** @hide */
     @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = {
             LineBreaker.JUSTIFICATION_MODE_NONE,
-            LineBreaker.JUSTIFICATION_MODE_INTER_WORD
+            LineBreaker.JUSTIFICATION_MODE_INTER_WORD,
+            LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface JustificationMode {}
@@ -168,6 +169,13 @@
     public static final int JUSTIFICATION_MODE_INTER_WORD =
             LineBreaker.JUSTIFICATION_MODE_INTER_WORD;
 
+    /**
+     * Value for justification mode indicating the text is justified by stretching letter spacing.
+     */
+    @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+    public static final int JUSTIFICATION_MODE_INTER_CHARACTER =
+            LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER;
+
     /*
      * Line spacing multiplier for default line spacing.
      */
@@ -809,7 +817,7 @@
                         getEllipsisStart(lineNum) + getEllipsisCount(lineNum),
                         isFallbackLineSpacingEnabled());
                 if (justify) {
-                    tl.justify(right - left - indentWidth);
+                    tl.justify(mJustificationMode, right - left - indentWidth);
                 }
                 tl.draw(canvas, x, ltop, lbaseline, lbottom);
             }
@@ -1058,7 +1066,7 @@
                     getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                     isFallbackLineSpacingEnabled());
             if (isJustificationRequired(line)) {
-                tl.justify(getJustifyWidth(line));
+                tl.justify(mJustificationMode, getJustifyWidth(line));
             }
             tl.metrics(null, rectF, false, null);
 
@@ -1794,7 +1802,7 @@
                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                 isFallbackLineSpacingEnabled());
         if (isJustificationRequired(line)) {
-            tl.justify(getJustifyWidth(line));
+            tl.justify(mJustificationMode, getJustifyWidth(line));
         }
         final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
         TextLine.recycle(tl);
@@ -1882,7 +1890,7 @@
                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                 isFallbackLineSpacingEnabled());
         if (isJustificationRequired(line)) {
-            tl.justify(getJustifyWidth(line));
+            tl.justify(mJustificationMode, getJustifyWidth(line));
         }
         final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
         TextLine.recycle(tl);
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index 2175b47..224e5d8 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -100,9 +100,25 @@
 
     // Additional width of whitespace for justification. This value is per whitespace, thus
     // the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces).
-    private float mAddedWidthForJustify;
+    private float mAddedWordSpacingInPx;
+    private float mAddedLetterSpacingInPx;
     private boolean mIsJustifying;
 
+    @VisibleForTesting
+    public float getAddedWordSpacingInPx() {
+        return mAddedWordSpacingInPx;
+    }
+
+    @VisibleForTesting
+    public float getAddedLetterSpacingInPx() {
+        return mAddedLetterSpacingInPx;
+    }
+
+    @VisibleForTesting
+    public boolean isJustifying() {
+        return mIsJustifying;
+    }
+
     private final TextPaint mWorkPaint = new TextPaint();
     private final TextPaint mActivePaint = new TextPaint();
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -259,7 +275,7 @@
             }
         }
         mTabs = tabStops;
-        mAddedWidthForJustify = 0;
+        mAddedWordSpacingInPx = 0;
         mIsJustifying = false;
 
         mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;
@@ -274,19 +290,42 @@
      * Justify the line to the given width.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    public void justify(float justifyWidth) {
+    public void justify(@Layout.JustificationMode int justificationMode, float justifyWidth) {
         int end = mLen;
         while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
             end--;
         }
-        final int spaces = countStretchableSpaces(0, end);
-        if (spaces == 0) {
-            // There are no stretchable spaces, so we can't help the justification by adding any
-            // width.
-            return;
+        if (justificationMode == Layout.JUSTIFICATION_MODE_INTER_WORD) {
+            float width = Math.abs(measure(end, false, null, null, null));
+            final int spaces = countStretchableSpaces(0, end);
+            if (spaces == 0) {
+                // There are no stretchable spaces, so we can't help the justification by adding any
+                // width.
+                return;
+            }
+            mAddedWordSpacingInPx = (justifyWidth - width) / spaces;
+            mAddedLetterSpacingInPx = 0;
+        } else {  // justificationMode == Layout.JUSTIFICATION_MODE_INTER_CHARACTER
+            LineInfo lineInfo = new LineInfo();
+            float width = Math.abs(measure(end, false, null, null, lineInfo));
+
+            int lettersCount = lineInfo.getClusterCount();
+            if (lettersCount < 2) {
+                return;
+            }
+            mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1);
+            if (mAddedLetterSpacingInPx > 0.03) {
+                // If the letter spacing is more than 0.03em, the ligatures are automatically
+                // disabled, so re-calculate everything without ligatures.
+                final String oldFontFeatures = mPaint.getFontFeatureSettings();
+                mPaint.setFontFeatureSettings(oldFontFeatures + ", \"liga\" off, \"cliga\" off");
+                width = Math.abs(measure(end, false, null, null, lineInfo));
+                lettersCount = lineInfo.getClusterCount();
+                mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1);
+                mPaint.setFontFeatureSettings(oldFontFeatures);
+            }
+            mAddedWordSpacingInPx = 0;
         }
-        final float width = Math.abs(measure(end, false, null, null, null));
-        mAddedWidthForJustify = (justifyWidth - width) / spaces;
         mIsJustifying = true;
     }
 
@@ -529,6 +568,9 @@
             throw new IndexOutOfBoundsException(
                     "offset(" + offset + ") should be less than line limit(" + mLen + ")");
         }
+        if (lineInfo != null) {
+            lineInfo.setClusterCount(0);
+        }
         final int target = trailing ? offset - 1 : offset;
         if (target < 0) {
             return 0;
@@ -1076,7 +1118,8 @@
         TextPaint wp = mWorkPaint;
         wp.set(mPaint);
         if (mIsJustifying) {
-            wp.setWordSpacing(mAddedWidthForJustify);
+            wp.setWordSpacing(mAddedWordSpacingInPx);
+            wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize());  // Convert to Em
         }
 
         int spanStart = runStart;
@@ -1277,7 +1320,8 @@
             @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo,
             int runFlag) {
         if (mIsJustifying) {
-            wp.setWordSpacing(mAddedWidthForJustify);
+            wp.setWordSpacing(mAddedWordSpacingInPx);
+            wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize());  // Convert to Em
         }
         // Get metrics first (even for empty strings or "0" width runs)
         if (drawBounds != null && fmi == null) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e0bda91..3c36227 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -26,7 +26,6 @@
 import static android.view.InputDevice.SOURCE_CLASS_NONE;
 import static android.view.InsetsSource.ID_IME;
 import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
-import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
 import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
 import static android.view.View.PFLAG_DRAW_ANIMATION;
 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
@@ -1012,10 +1011,8 @@
     // Used to check if there were any view invalidations in
     // the previous time frame (FRAME_RATE_IDLENESS_REEVALUATE_TIME).
     private boolean mHasInvalidation = false;
-    // Used to check if it is in the frame rate boosting period.
+    // Used to check if it is in the touch boosting period.
     private boolean mIsFrameRateBoosting = false;
-    // Used to check if it is in touch boosting period.
-    private boolean mIsTouchBoosting = false;
     // Used to check if there is a message in the message queue
     // for idleness handling.
     private boolean mHasIdledMessage = false;
@@ -6424,12 +6421,11 @@
                      * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME).
                      */
                     mIsFrameRateBoosting = false;
-                    mIsTouchBoosting = false;
                     setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory,
                             mLastPreferredFrameRateCategory));
                     break;
                 case MSG_CHECK_INVALIDATION_IDLE:
-                    if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) {
+                    if (!mHasInvalidation && !mIsFrameRateBoosting) {
                         mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
                         setPreferredFrameRateCategory(mPreferredFrameRateCategory);
                         mHasIdledMessage = false;
@@ -7452,7 +7448,7 @@
             // For the variable refresh rate project
             if (handled && shouldTouchBoost(action, mWindowAttributes.type)) {
                 // set the frame rate to the maximum value.
-                mIsTouchBoosting = true;
+                mIsFrameRateBoosting = true;
                 setPreferredFrameRateCategory(mPreferredFrameRateCategory);
             }
             /**
@@ -12204,16 +12200,8 @@
             return;
         }
 
-        int frameRateCategory = mIsTouchBoosting
-                ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory;
-
-        // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT
-        // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction.
-        // FRAME_RATE_CATEGORY_HIGH is for boosting without user interaction
-        // (e.g., Window Initialization).
-        if (mIsFrameRateBoosting || mInsetsAnimationRunning) {
-            frameRateCategory = FRAME_RATE_CATEGORY_HIGH;
-        }
+        int frameRateCategory = mIsFrameRateBoosting || mInsetsAnimationRunning
+                ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory;
 
         try {
             if (mLastPreferredFrameRateCategory != frameRateCategory) {
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index efae57c..a11ac7c 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -94,6 +94,13 @@
 }
 
 flag {
+    name: "skip_accessibility_warning_dialog_for_trusted_services"
+    namespace: "accessibility"
+    description: "Skips showing the accessibility warning dialog for trusted services."
+    bug: "303511250"
+}
+
+flag {
     namespace: "accessibility"
     name: "update_always_on_a11y_service"
     description: "Updates the Always-On A11yService state when the user changes the enablement of the shortcut."
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index c20b278..7f5331b 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -167,6 +167,11 @@
                     + ", reported config=" + currentConfig
                     + ", updated config=" + newConfig);
         }
+        // Update display first. In case callers want to obtain display information(
+        // ex: DisplayMetrics) in #onConfigurationChanged callback.
+        if (displayChanged) {
+            context.updateDisplay(newDisplayId);
+        }
         if (shouldUpdateResources) {
             // TODO(ag/9789103): update resource manager logic to track non-activity tokens
             mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId);
@@ -195,9 +200,6 @@
                 }
             }
         }
-        if (displayChanged) {
-            context.updateDisplay(newDisplayId);
-        }
     }
 
     /**
diff --git a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java
index ce9ab82..2ff6225 100644
--- a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java
+++ b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java
@@ -21,6 +21,7 @@
 import android.app.backup.BackupDataInputStream;
 import android.app.backup.BackupDataOutput;
 import android.app.backup.BackupHelper;
+import android.app.backup.BackupHelperWithLogger;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SyncAdapterType;
@@ -56,7 +57,7 @@
  * sync settings are backed up as a JSON object containing all the necessary information for
  * restoring the sync settings later.
  */
-public class AccountSyncSettingsBackupHelper implements BackupHelper {
+public class AccountSyncSettingsBackupHelper extends BackupHelperWithLogger {
 
     private static final String TAG = "AccountSyncSettingsBackupHelper";
     private static final boolean DEBUG = false;
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index c8fd246..656cc3e 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -188,8 +188,6 @@
                 "android_os_SharedMemory.cpp",
                 "android_os_storage_StorageManager.cpp",
                 "android_os_UEventObserver.cpp",
-                "android_os_VintfObject.cpp",
-                "android_os_VintfRuntimeInfo.cpp",
                 "android_os_incremental_IncrementalManager.cpp",
                 "android_net_LocalSocketImpl.cpp",
                 "android_service_DataLoaderService.cpp",
@@ -280,6 +278,7 @@
                 "libdmabufinfo",
                 "libgif",
                 "libgui_window_info_static",
+                "libkernelconfigs",
                 "libseccomp_policy",
                 "libgrallocusage",
                 "libscrypt_static",
@@ -350,7 +349,6 @@
                 "libnativeloader_lazy",
                 "libmemunreachable",
                 "libhidlbase",
-                "libvintf",
                 "libnativedisplay",
                 "libnativewindow",
                 "libdl",
@@ -458,8 +456,25 @@
                 // (e.g. gDefaultServiceManager)
                 "libbinder",
                 "libhidlbase", // libhwbinder is in here
-                "libvintf",
             ],
         },
     },
 }
+
+cc_library_shared {
+    name: "libvintf_jni",
+
+    cpp_std: "gnu++20",
+
+    srcs: [
+        "android_os_VintfObject.cpp",
+        "android_os_VintfRuntimeInfo.cpp",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libnativehelper",
+        "libvintf",
+    ],
+}
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 7a16318..aa63f4f 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -152,8 +152,6 @@
 extern int register_android_os_Parcel(JNIEnv* env);
 extern int register_android_os_PerformanceHintManager(JNIEnv* env);
 extern int register_android_os_SELinux(JNIEnv* env);
-extern int register_android_os_VintfObject(JNIEnv *env);
-extern int register_android_os_VintfRuntimeInfo(JNIEnv *env);
 extern int register_android_os_storage_StorageManager(JNIEnv* env);
 extern int register_android_os_SystemProperties(JNIEnv *env);
 extern int register_android_os_SystemClock(JNIEnv* env);
@@ -1545,8 +1543,6 @@
         REG_JNI(register_android_os_NativeHandle),
         REG_JNI(register_android_os_ServiceManager),
         REG_JNI(register_android_os_storage_StorageManager),
-        REG_JNI(register_android_os_VintfObject),
-        REG_JNI(register_android_os_VintfRuntimeInfo),
         REG_JNI(register_android_service_DataLoaderService),
         REG_JNI(register_android_view_DisplayEventReceiver),
         REG_JNI(register_android_view_Surface),
diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp
index de1ce4e..1504a00 100644
--- a/core/jni/android_os_Debug.cpp
+++ b/core/jni/android_os_Debug.cpp
@@ -52,7 +52,7 @@
 #include <memunreachable/memunreachable.h>
 #include <android-base/strings.h>
 #include "android_os_Debug.h"
-#include <vintf/VintfObject.h>
+#include <vintf/KernelConfigs.h>
 
 namespace android
 {
@@ -1004,10 +1004,9 @@
     } cfg_state = CONFIG_UNKNOWN;
 
     if (cfg_state == CONFIG_UNKNOWN) {
-        auto runtime_info = vintf::VintfObject::GetInstance()->getRuntimeInfo(
-                vintf::RuntimeInfo::FetchFlag::CONFIG_GZ);
-        CHECK(runtime_info != nullptr) << "Kernel configs cannot be fetched. b/151092221";
-        const std::map<std::string, std::string>& configs = runtime_info->kernelConfigs();
+        std::map<std::string, std::string> configs;
+        const status_t result = android::kernelconfigs::LoadKernelConfigs(&configs);
+        CHECK(result == OK) << "Kernel configs could not be fetched. b/151092221";
         std::map<std::string, std::string>::const_iterator it = configs.find("CONFIG_VMAP_STACK");
         cfg_state = (it != configs.end() && it->second == "y") ? CONFIG_SET : CONFIG_UNSET;
     }
diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp
index 477bd09..734b5f4 100644
--- a/core/jni/android_os_HwBinder.cpp
+++ b/core/jni/android_os_HwBinder.cpp
@@ -39,7 +39,6 @@
 #include <hwbinder/ProcessState.h>
 #include <nativehelper/ScopedLocalRef.h>
 #include <nativehelper/ScopedUtfChars.h>
-#include <vintf/parse_string.h>
 #include <utils/misc.h>
 
 #include "core_jni_helpers.h"
diff --git a/core/jni/android_os_VintfObject.cpp b/core/jni/android_os_VintfObject.cpp
index a5b2f65..ce4a337 100644
--- a/core/jni/android_os_VintfObject.cpp
+++ b/core/jni/android_os_VintfObject.cpp
@@ -17,16 +17,14 @@
 #define LOG_TAG "VintfObject"
 //#define LOG_NDEBUG 0
 #include <android-base/logging.h>
-
-#include <vector>
-#include <string>
-
-#include <nativehelper/JNIHelp.h>
 #include <vintf/VintfObject.h>
 #include <vintf/parse_string.h>
 #include <vintf/parse_xml.h>
 
-#include "core_jni_helpers.h"
+#include <vector>
+#include <string>
+
+#include "jni_wrappers.h"
 
 static jclass gString;
 static jclass gHashMapClazz;
@@ -94,7 +92,7 @@
     return toJavaStringArray(env, cStrings);
 }
 
-static jint android_os_VintfObject_verifyBuildAtBoot(JNIEnv* env, jclass) {
+static jint android_os_VintfObject_verifyBuildAtBoot(JNIEnv*, jclass) {
     std::string error;
     // Use temporary VintfObject, not the shared instance, to release memory
     // after check.
@@ -204,4 +202,23 @@
             NELEM(gVintfObjectMethods));
 }
 
-};
+extern int register_android_os_VintfRuntimeInfo(JNIEnv* env);
+
+} // namespace android
+
+jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
+    JNIEnv* env = NULL;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)) {
+        return JNI_ERR;
+    }
+
+    if (android::register_android_os_VintfObject(env) < 0) {
+        return JNI_ERR;
+    }
+
+    if (android::register_android_os_VintfRuntimeInfo(env) < 0) {
+        return JNI_ERR;
+    }
+
+    return JNI_VERSION_1_6;
+}
diff --git a/core/jni/android_os_VintfRuntimeInfo.cpp b/core/jni/android_os_VintfRuntimeInfo.cpp
index b0271b9..7c2f588 100644
--- a/core/jni/android_os_VintfRuntimeInfo.cpp
+++ b/core/jni/android_os_VintfRuntimeInfo.cpp
@@ -17,23 +17,22 @@
 #define LOG_TAG "VintfRuntimeInfo"
 //#define LOG_NDEBUG 0
 
-#include <nativehelper/JNIHelp.h>
 #include <vintf/VintfObject.h>
 #include <vintf/parse_string.h>
 #include <vintf/parse_xml.h>
 
-#include "core_jni_helpers.h"
+#include "jni_wrappers.h"
 
 namespace android {
 
 using vintf::RuntimeInfo;
 using vintf::VintfObject;
 
-#define MAP_STRING_METHOD(javaMethod, cppString, flags)                                  \
-    static jstring android_os_VintfRuntimeInfo_##javaMethod(JNIEnv* env, jclass clazz) { \
-        std::shared_ptr<const RuntimeInfo> info = VintfObject::GetRuntimeInfo(flags);    \
-        if (info == nullptr) return nullptr;                                             \
-        return env->NewStringUTF((cppString).c_str());                                   \
+#define MAP_STRING_METHOD(javaMethod, cppString, flags)                               \
+    static jstring android_os_VintfRuntimeInfo_##javaMethod(JNIEnv* env, jclass) {    \
+        std::shared_ptr<const RuntimeInfo> info = VintfObject::GetRuntimeInfo(flags); \
+        if (info == nullptr) return nullptr;                                          \
+        return env->NewStringUTF((cppString).c_str());                                \
     }
 
 MAP_STRING_METHOD(getCpuInfo, info->cpuInfo(), RuntimeInfo::FetchFlag::CPU_INFO);
@@ -49,9 +48,7 @@
 MAP_STRING_METHOD(getBootVbmetaAvbVersion, vintf::to_string(info->bootVbmetaAvbVersion()),
                   RuntimeInfo::FetchFlag::AVB);
 
-
-static jlong android_os_VintfRuntimeInfo_getKernelSepolicyVersion(JNIEnv *env, jclass clazz)
-{
+static jlong android_os_VintfRuntimeInfo_getKernelSepolicyVersion(JNIEnv*, jclass) {
     std::shared_ptr<const RuntimeInfo> info =
             VintfObject::GetRuntimeInfo(RuntimeInfo::FetchFlag::POLICYVERS);
     if (info == nullptr) return 0;
diff --git a/core/jni/core_jni_helpers.h b/core/jni/core_jni_helpers.h
index 210dc89..769fa72 100644
--- a/core/jni/core_jni_helpers.h
+++ b/core/jni/core_jni_helpers.h
@@ -22,6 +22,8 @@
 #include <nativehelper/scoped_utf_chars.h>
 #include <android_runtime/AndroidRuntime.h>
 
+#include "jni_wrappers.h"
+
 // Host targets (layoutlib) do not differentiate between regular and critical native methods,
 // and they need all the JNI methods to have JNIEnv* and jclass/jobject as their first two arguments.
 // The following macro allows to have those arguments when compiling for host while omitting them when
@@ -36,60 +38,6 @@
 
 namespace android {
 
-// Defines some helpful functions.
-
-static inline jclass FindClassOrDie(JNIEnv* env, const char* class_name) {
-    jclass clazz = env->FindClass(class_name);
-    LOG_ALWAYS_FATAL_IF(clazz == NULL, "Unable to find class %s", class_name);
-    return clazz;
-}
-
-static inline jfieldID GetFieldIDOrDie(JNIEnv* env, jclass clazz, const char* field_name,
-                                       const char* field_signature) {
-    jfieldID res = env->GetFieldID(clazz, field_name, field_signature);
-    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find field %s with signature %s", field_name,
-                        field_signature);
-    return res;
-}
-
-static inline jmethodID GetMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name,
-                                         const char* method_signature) {
-    jmethodID res = env->GetMethodID(clazz, method_name, method_signature);
-    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find method %s with signature %s", method_name,
-                        method_signature);
-    return res;
-}
-
-static inline jfieldID GetStaticFieldIDOrDie(JNIEnv* env, jclass clazz, const char* field_name,
-                                             const char* field_signature) {
-    jfieldID res = env->GetStaticFieldID(clazz, field_name, field_signature);
-    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static field %s with signature %s", field_name,
-                        field_signature);
-    return res;
-}
-
-static inline jmethodID GetStaticMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name,
-                                               const char* method_signature) {
-    jmethodID res = env->GetStaticMethodID(clazz, method_name, method_signature);
-    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static method %s with signature %s",
-                        method_name, method_signature);
-    return res;
-}
-
-template <typename T>
-static inline T MakeGlobalRefOrDie(JNIEnv* env, T in) {
-    jobject res = env->NewGlobalRef(in);
-    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to create global reference.");
-    return static_cast<T>(res);
-}
-
-static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className,
-                                       const JNINativeMethod* gMethods, int numMethods) {
-    int res = AndroidRuntime::registerNativeMethods(env, className, gMethods, numMethods);
-    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
-    return res;
-}
-
 /**
  * Returns the result of invoking java.lang.ref.Reference.get() on a Reference object.
  */
diff --git a/core/jni/jni_wrappers.h b/core/jni/jni_wrappers.h
new file mode 100644
index 0000000..3b29e30
--- /dev/null
+++ b/core/jni/jni_wrappers.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+// JNI wrappers for better logging
+
+#include <jni.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+
+namespace android {
+
+static inline jclass FindClassOrDie(JNIEnv* env, const char* class_name) {
+    jclass clazz = env->FindClass(class_name);
+    LOG_ALWAYS_FATAL_IF(clazz == NULL, "Unable to find class %s", class_name);
+    return clazz;
+}
+
+static inline jfieldID GetFieldIDOrDie(JNIEnv* env, jclass clazz, const char* field_name,
+                                       const char* field_signature) {
+    jfieldID res = env->GetFieldID(clazz, field_name, field_signature);
+    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find field %s with signature %s", field_name,
+                        field_signature);
+    return res;
+}
+
+static inline jmethodID GetMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name,
+                                         const char* method_signature) {
+    jmethodID res = env->GetMethodID(clazz, method_name, method_signature);
+    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find method %s with signature %s", method_name,
+                        method_signature);
+    return res;
+}
+
+static inline jfieldID GetStaticFieldIDOrDie(JNIEnv* env, jclass clazz, const char* field_name,
+                                             const char* field_signature) {
+    jfieldID res = env->GetStaticFieldID(clazz, field_name, field_signature);
+    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static field %s with signature %s", field_name,
+                        field_signature);
+    return res;
+}
+
+static inline jmethodID GetStaticMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name,
+                                               const char* method_signature) {
+    jmethodID res = env->GetStaticMethodID(clazz, method_name, method_signature);
+    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static method %s with signature %s",
+                        method_name, method_signature);
+    return res;
+}
+
+template <typename T>
+static inline T MakeGlobalRefOrDie(JNIEnv* env, T in) {
+    jobject res = env->NewGlobalRef(in);
+    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to create global reference.");
+    return static_cast<T>(res);
+}
+
+static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className,
+                                       const JNINativeMethod* gMethods, int numMethods) {
+    int res = jniRegisterNativeMethods(env, className, gMethods, numMethods);
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+    return res;
+}
+
+} // namespace android
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b199420..52cf679 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3569,6 +3569,13 @@
     <permission android:name="android.permission.MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION"
                 android:protectionLevel="internal|role" />
 
+    <!-- Allows an application to set policy related to <a
+    href="https://www.threadgroup.org">Thread</a> network.
+        @FlaggedApi("com.android.net.thread.flags.thread_user_restriction_enabled")
+    -->
+    <permission android:name="android.permission.MANAGE_DEVICE_POLICY_THREAD_NETWORK"
+                android:protectionLevel="internal|role" />
+
     <!-- Allows an application to set policy related to windows.
         <p>{@link Manifest.permission#MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL} is
         required to call APIs protected by this permission on users different to the calling user.
@@ -4961,7 +4968,7 @@
          <p>Protection level: signature
     -->
     <permission android:name="android.permission.BIND_NFC_SERVICE"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|module" />
 
     <!-- Must be required by a {@link android.service.quickaccesswallet.QuickAccessWalletService}
          to ensure that only the system can bind to it.
diff --git a/core/res/res/drawable/autofill_half_sheet_divider.xml b/core/res/res/drawable/autofill_half_sheet_divider.xml
new file mode 100644
index 0000000..1a96c7d
--- /dev/null
+++ b/core/res/res/drawable/autofill_half_sheet_divider.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copied from //frameworks/base/core/res/res/drawable/list_divider_material.xml. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:tint="@color/foreground_material_light">
+  <solid android:color="#1f000000" />
+  <size
+      android:height="1dp"
+      android:width="1dp"/>
+</shape>
\ No newline at end of file
diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml
index 27f8138..ddedca2 100644
--- a/core/res/res/layout/autofill_save.xml
+++ b/core/res/res/layout/autofill_save.xml
@@ -31,11 +31,11 @@
         android:gravity="center_horizontal"
         android:orientation="vertical">
         <ScrollView
+            android:id="@+id/autofill_sheet_scroll_view"
             android:layout_width="fill_parent"
             android:layout_height="0dp"
             android:fillViewport="true"
-            android:layout_weight="1"
-            android:layout_marginBottom="8dp">
+            android:layout_weight="1">
             <LinearLayout
                 android:layout_marginStart="@dimen/autofill_save_outer_margin"
                 android:layout_marginEnd="@dimen/autofill_save_outer_margin"
@@ -66,16 +66,25 @@
                     android:layout_height="wrap_content"
                     android:minHeight="0dp"
                     android:visibility="gone"/>
-
+                <View
+                    android:id="@+id/autofill_sheet_scroll_view_space"
+                    android:layout_width="match_parent"
+                    android:layout_height="16dp"/>
             </LinearLayout>
         </ScrollView>
 
+        <View
+            android:id="@+id/autofill_sheet_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            style="@style/AutofillHalfSheetDivider" />
+
         <com.android.internal.widget.ButtonBarLayout
             android:layout_width="match_parent"
             android:layout_height="48dp"
             android:layout_gravity="end"
             android:clipToPadding="false"
-            android:layout_marginTop="16dp"
+            android:layout_marginTop="8dp"
             android:layout_marginBottom="8dp"
             android:theme="@style/Theme.DeviceDefault.AutofillHalfScreenDialogButton"
             android:orientation="horizontal"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5be29a6..5e2aacd 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -57,6 +57,7 @@
         <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_ethernet</xliff:g></item>
+        <item><xliff:g id="id">@string/status_bar_oem_satellite</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_mobile</xliff:g></item>
@@ -102,6 +103,7 @@
     <string translatable="false" name="status_bar_call_strength">call_strength</string>
     <string translatable="false" name="status_bar_sensors_off">sensors_off</string>
     <string translatable="false" name="status_bar_screen_record">screen_record</string>
+    <string translatable="false" name="status_bar_oem_satellite">satellite</string>
 
     <!-- Flag indicating whether the surface flinger has limited
          alpha compositing functionality in hardware.  If set, the window
@@ -4495,6 +4497,16 @@
     <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. -->
     <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string>
 
+    <!-- Array of component names, each flattened to a string, for accessibility services that
+         can be enabled by the user without showing a warning prompt. These services must be
+         preinstalled. -->
+    <string-array translatable="false" name="config_trustedAccessibilityServices">
+        <!--
+        <item>com.example.package.first/com.example.class.FirstService</item>
+        <item>com.example.package.second/com.example.class.SecondService</item>
+        -->
+    </string-array>
+
     <!-- Warning: This API can be dangerous when not implemented properly. In particular,
          escrow token must NOT be retrievable from device storage. In other words, either
          escrow token is not stored on device or its ciphertext is stored on device while
@@ -5334,20 +5346,19 @@
      and a second time clipped to the fill level to indicate charge -->
     <bool name="config_batterymeterDualTone">false</bool>
 
-    <!-- The default refresh rate for a given device. Change this value to set a higher default
-         refresh rate. If the hardware composer on the device supports display modes with a higher
-         refresh rate than the default value specified here, the framework may use those higher
-         refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling
-         setFrameRate().
-         If a non-zero value is set for config_defaultPeakRefreshRate, then
-         config_defaultRefreshRate may be set to 0, in which case the value set for
-         config_defaultPeakRefreshRate will act as the default frame rate. -->
-    <integer name="config_defaultRefreshRate">60</integer>
+    <!-- The default refresh rate for a given device. This value is used to set the
+         global refresh rate vote, and when set to zero it has no effect on the vote.
+         If this value is non-zero but the hardware composer on the device supports
+         display modes with higher refresh rates, the framework may use those higher
+         refresh rate modes if an app chooses one by setting preferredDisplayModeId
+         or calling setFrameRate().-->
+    <integer name="config_defaultRefreshRate">0</integer>
 
-    <!-- The default peak refresh rate for a given device. Change this value if you want to prevent
-         the framework from using higher refresh rates, even if display modes with higher refresh
-         rates are available from hardware composer. Only has an effect if the value is
-         non-zero. -->
+    <!-- The default peak refresh rate for a given device. This value is used to set the
+         global peak refresh rate vote, and when set to zero it has no effect on the vote.
+         Change this value to non-zero if you want to prevent the framework from using higher
+         refresh rates, even if display modes with higher refresh rates are available from
+         hardware composer. -->
     <integer name="config_defaultPeakRefreshRate">0</integer>
 
     <!-- External display peak refresh rate for the given device. Change this value if you want to
@@ -6890,4 +6901,8 @@
     <!-- Defines suitability of the built-in speaker route.
          Refer to {@link MediaRoute2Info} to see supported values.  -->
     <integer name="config_mediaRouter_builtInSpeakerSuitability">0</integer>
+
+    <!-- Whether to show a percentage text next to the progressbar while preparing to update the
+         device -->
+    <bool name="config_showPercentageTextDuringRebootToUpdate">true</bool>
 </resources>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 619ec31..22d028c 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1515,6 +1515,11 @@
         <item name="background">@drawable/btn_outlined</item>
     </style>
 
+    <!-- @hide Divider for Autofill half screen dialog -->
+    <style name="AutofillHalfSheetDivider">
+        <item name="android:background">@drawable/autofill_half_sheet_divider</item>
+    </style>
+
     <!-- @hide Autofill background for popup window (not for fullscreen) -->
     <style name="AutofillDatasetPicker">
         <item name="elevation">4dp</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d12ef2b..33ea02a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3198,6 +3198,7 @@
   <java-symbol type="string" name="status_bar_camera" />
   <java-symbol type="string" name="status_bar_sensors_off" />
   <java-symbol type="string" name="status_bar_screen_record" />
+  <java-symbol type="string" name="status_bar_oem_satellite" />
 
   <!-- Locale picker -->
   <java-symbol type="id" name="locale_search_menu" />
@@ -3629,6 +3630,7 @@
   <java-symbol type="string" name="config_defaultAccessibilityService" />
   <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" />
   <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" />
+  <java-symbol type="array" name="config_trustedAccessibilityServices" />
 
   <java-symbol type="string" name="accessibility_select_shortcut_menu_title" />
   <java-symbol type="string" name="accessibility_edit_shortcut_menu_button_title" />
@@ -3702,6 +3704,10 @@
   <java-symbol type="id" name="autofill_dataset_list"/>
   <java-symbol type="id" name="autofill_dataset_picker"/>
   <java-symbol type="id" name="autofill_dataset_title" />
+  <java-symbol type="id" name="autofill_sheet_divider"/>
+  <java-symbol type="id" name="autofill_sheet_scroll_view"/>
+  <java-symbol type="id" name="autofill_sheet_scroll_view_space"/>
+
   <java-symbol type="id" name="autofill_save_custom_subtitle" />
   <java-symbol type="id" name="autofill_save_icon" />
   <java-symbol type="id" name="autofill_save_no" />
@@ -5311,4 +5317,7 @@
 
   <!-- Android MediaRouter framework configs. -->
   <java-symbol type="integer" name="config_mediaRouter_builtInSpeakerSuitability" />
+
+  <!-- Shutdown thread config flags -->
+  <java-symbol type="bool" name="config_showPercentageTextDuringRebootToUpdate" />
 </resources>
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java
index bbac69f..4f9b269 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -728,10 +729,10 @@
 
     @Test
     public void equals_withFmBandConfigsOfDifferentAfSupportValues() {
-        RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig(
-                new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT,
-                        FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED,
-                        !AF_SUPPORTED, EA_SUPPORTED));
+        RadioManager.FmBandConfig.Builder builder = new RadioManager.FmBandConfig.Builder(
+                createFmBandDescriptor()).setStereo(STEREO_SUPPORTED).setRds(RDS_SUPPORTED)
+                .setTa(TA_SUPPORTED).setAf(!AF_SUPPORTED).setEa(EA_SUPPORTED);
+        RadioManager.FmBandConfig fmBandConfigCompared = builder.build();
 
         assertWithMessage("FM Band Config of different af support value")
                 .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigCompared);
@@ -1300,6 +1301,18 @@
     }
 
     @Test
+    public void addAnnouncementListener_withListenerAddedBeforeAndCloseException_throws()
+            throws Exception {
+        createRadioManager();
+        Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+        doThrow(new RemoteException()).when(mCloseHandleMock).close();
+
+        assertThrows(RuntimeException.class,
+                () -> mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener));
+    }
+
+    @Test
     public void addAnnouncementListener_whenServiceDied_throwException() throws Exception {
         createRadioManager();
         String exceptionMessage = "service is dead";
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
index 4841711..4cda26d 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
@@ -125,6 +125,16 @@
     }
 
     @Test
+    public void close_forTunerAdapterCalledTwice() throws Exception {
+        mRadioTuner.close();
+        verify(mTunerMock).close();
+
+        mRadioTuner.close();
+
+        verify(mTunerMock).close();
+    }
+
+    @Test
     public void setConfiguration_forTunerAdapter() throws Exception {
         int status = mRadioTuner.setConfiguration(TEST_BAND_CONFIG);
 
@@ -134,6 +144,12 @@
     }
 
     @Test
+    public void setConfiguration_withNull_fails() throws Exception {
+        assertWithMessage("Status for setting configuration with null")
+                .that(mRadioTuner.setConfiguration(null)).isEqualTo(RadioManager.STATUS_BAD_VALUE);
+    }
+
+    @Test
     public void setConfiguration_withInvalidParameters_fails() throws Exception {
         doThrow(new IllegalArgumentException()).when(mTunerMock).setConfiguration(any());
 
@@ -840,6 +856,15 @@
     }
 
     @Test
+    public void onTuneFailed_withDeadService() throws Exception {
+        mTunerCallback.onTuneFailed(RadioManager.STATUS_DEAD_OBJECT, FM_SELECTOR);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onTuneFailed(
+                RadioManager.STATUS_DEAD_OBJECT, FM_SELECTOR);
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_SERVER_DIED);
+    }
+
+    @Test
     public void onProgramListChanged_forTunerCallbackAdapter() throws Exception {
         mTunerCallback.onProgramListChanged();
 
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
index e118c98d..3ee565f 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
@@ -403,4 +403,41 @@
         }
         assertFalse(allowed);
     }
+
+    /** Return true if the path is in the list of strings. */
+    private boolean isConcurrent(String path) throws Exception {
+        path = new File(path).toPath().toRealPath().toString();
+        return SQLiteDatabase.getConcurrentDatabasePaths().contains(path);
+    }
+
+    @Test
+    public void testDuplicateDatabases() throws Exception {
+        // The two database paths in this test are assumed not to have been opened earlier in this
+        // process.
+
+        // A database path that will be opened twice.
+        final String dbName = "never-used-db.db";
+        final File dbFile = mContext.getDatabasePath(dbName);
+        final String dbPath = dbFile.getPath();
+
+        // A database path that will be opened only once.
+        final String okName = "never-used-ok.db";
+        final File okFile = mContext.getDatabasePath(okName);
+        final String okPath = okFile.getPath();
+
+        SQLiteDatabase db1 = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
+        assertFalse(isConcurrent(dbPath));
+        SQLiteDatabase db2 = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
+        assertTrue(isConcurrent(dbPath));
+        db1.close();
+        assertTrue(isConcurrent(dbPath));
+        db2.close();
+        assertTrue(isConcurrent(dbPath));
+
+        SQLiteDatabase db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null);
+        db3.close();
+        db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null);
+        assertFalse(isConcurrent(okPath));
+        db3.close();
+    }
 }
diff --git a/core/tests/coretests/src/android/text/TextLineJustificationTest.kt b/core/tests/coretests/src/android/text/TextLineJustificationTest.kt
new file mode 100644
index 0000000..a525615
--- /dev/null
+++ b/core/tests/coretests/src/android/text/TextLineJustificationTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text
+
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TextLineJustificationTest {
+
+    @Rule
+    @JvmField
+    val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+    private val PAINT = TextPaint().apply {
+        textSize = 10f // make 1em = 10px
+    }
+
+    private fun makeTextLine(cs: CharSequence, paint: TextPaint) = TextLine.obtain().apply {
+        set(paint, cs, 0, cs.length, Layout.DIR_LEFT_TO_RIGHT, Layout.DIRS_ALL_LEFT_TO_RIGHT, false,
+                null, 0, 0, false)
+    }
+
+    private fun getClusterCount(cs: CharSequence, paint: TextPaint) = TextLine.LineInfo().apply {
+        makeTextLine(cs, paint).also {
+            it.metrics(null, null, false, this)
+            TextLine.recycle(it)
+        }
+    }.clusterCount
+
+    fun justifyTest_WithoutJustify() {
+        val line = "Hello, World."
+        val tl = makeTextLine(line, PAINT)
+
+        // Without calling justify method, justifying should be false and all added spaces should
+        // be zeros.
+        assertThat(tl.isJustifying).isFalse()
+        assertThat(tl.addedWordSpacingInPx).isEqualTo(0)
+        assertThat(tl.addedLetterSpacingInPx).isEqualTo(0)
+    }
+
+    @Test
+    fun justifyTest_IntrCharacter_Latin() {
+        val line = "Hello, World."
+        val clusterCount = getClusterCount(line, PAINT)
+        val originalWidth = Layout.getDesiredWidth(line, PAINT)
+        val extraWidth = 100f
+
+        val tl = makeTextLine(line, PAINT)
+        tl.justify(Layout.JUSTIFICATION_MODE_INTER_CHARACTER, originalWidth + extraWidth)
+
+        assertThat(tl.isJustifying).isTrue()
+        assertThat(tl.addedWordSpacingInPx).isEqualTo(0)
+        assertThat(tl.addedLetterSpacingInPx).isEqualTo(extraWidth / (clusterCount - 1))
+
+        TextLine.recycle(tl)
+    }
+
+    @Test
+    fun justifyTest_IntrCharacter_Japanese() {
+        val line = "\u672C\u65E5\u306F\u6674\u5929\u306A\u308A\u3002"
+        val clusterCount = getClusterCount(line, PAINT)
+        val originalWidth = Layout.getDesiredWidth(line, PAINT)
+        val extraWidth = 100f
+
+        val tl = makeTextLine(line, PAINT)
+        tl.justify(Layout.JUSTIFICATION_MODE_INTER_CHARACTER, originalWidth + extraWidth)
+
+        assertThat(tl.isJustifying).isTrue()
+        assertThat(tl.addedWordSpacingInPx).isEqualTo(0)
+        assertThat(tl.addedLetterSpacingInPx).isEqualTo(extraWidth / (clusterCount - 1))
+
+        TextLine.recycle(tl)
+    }
+
+    @Test
+    fun justifyTest_IntrWord_Latin() {
+        val line = "Hello, World."
+        val originalWidth = Layout.getDesiredWidth(line, PAINT)
+        val extraWidth = 100f
+
+        val tl = makeTextLine(line, PAINT)
+        tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, originalWidth + extraWidth)
+
+        assertThat(tl.isJustifying).isTrue()
+        // This text contains only one whitespace, so word spacing should be same to the extraWidth.
+        assertThat(tl.addedWordSpacingInPx).isEqualTo(extraWidth)
+        assertThat(tl.addedLetterSpacingInPx).isEqualTo(0)
+
+        TextLine.recycle(tl)
+    }
+
+    @Test
+    fun justifyTest_IntrWord_Japanese() {
+        val line = "\u672C\u65E5\u306F\u6674\u0020\u5929\u306A\u308A\u3002"
+        val originalWidth = Layout.getDesiredWidth(line, PAINT)
+        val extraWidth = 100f
+
+        val tl = makeTextLine(line, PAINT)
+        tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, originalWidth + extraWidth)
+
+        assertThat(tl.isJustifying).isTrue()
+        // This text contains only one whitespace, so word spacing should be same to the extraWidth.
+        assertThat(tl.addedWordSpacingInPx).isEqualTo(extraWidth)
+        assertThat(tl.addedLetterSpacingInPx).isEqualTo(0)
+
+        TextLine.recycle(tl)
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/text/TextLineTest.java b/core/tests/coretests/src/android/text/TextLineTest.java
index a31992c..8ae5669 100644
--- a/core/tests/coretests/src/android/text/TextLineTest.java
+++ b/core/tests/coretests/src/android/text/TextLineTest.java
@@ -53,7 +53,7 @@
         final float originalWidth = tl.metrics(null, null, false, null);
         final float expandedWidth = 2 * originalWidth;
 
-        tl.justify(expandedWidth);
+        tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, expandedWidth);
         final float newWidth = tl.metrics(null, null, false, null);
         TextLine.recycle(tl);
         return Math.abs(newWidth - expandedWidth) < 0.5;
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index cfbda84..cf3eb12 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -19,7 +19,6 @@
 import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR;
 import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
 import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
-import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
 import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
 import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
 import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
@@ -576,13 +575,8 @@
             assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_LOW);
             viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL);
             assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL);
-            viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT);
-            assertEquals(viewRootImpl.getPreferredFrameRateCategory(),
-                    FRAME_RATE_CATEGORY_HIGH_HINT);
             viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH);
             assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
-            viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT);
-            assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
             viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL);
             assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
             viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW);
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 607b4bd..294b8ae 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -23,40 +23,48 @@
     <!-- Needed for Build.getSerial(), which is used to send a unique number for serial, per HUIG. -->
     <privapp-permissions package="android.car.usb.handler">
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.angle">
         <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.apps.tag">
         <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.backupconfirm">
         <permission name="android.permission.BACKUP"/>
         <permission name="android.permission.CRYPT_KEEPER"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.externalstorage">
         <permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
         <permission name="android.permission.WRITE_MEDIA_STORAGE"/>
         <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.imsserviceentitlement">
         <permission name="android.permission.MODIFY_PHONE_STATE" />
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.launcher3">
         <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.location.fused">
         <permission name="android.permission.INSTALL_LOCATION_PROVIDER"/>
         <permission name="android.permission.UPDATE_DEVICE_STATS"/>
         <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.managedprovisioning">
@@ -79,12 +87,14 @@
         <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
         <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/>
         <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.mms.service">
         <permission name="android.permission.BIND_CARRIER_MESSAGING_SERVICE"/>
         <permission name="android.permission.BIND_CARRIER_SERVICES"/>
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.mtp">
@@ -94,16 +104,19 @@
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
         <permission name="android.permission.WRITE_MEDIA_STORAGE"/>
         <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.musicfx">
         <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.networkrecommendation">
         <permission name="android.permission.SCORE_NETWORKS"/>
         <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
         <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.packageinstaller">
@@ -114,6 +127,7 @@
         <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
         <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
         <permission name="android.permission.PACKAGE_USAGE_STATS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.phone">
@@ -170,6 +184,7 @@
         <permission name="android.permission.LOG_COMPAT_CHANGE"/>
         <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
         <permission name="android.permission.UWB_PRIVILEGED"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.providers.calendar">
@@ -180,6 +195,7 @@
         <permission name="android.permission.USE_RESERVED_DISK"/>
         <permission name="android.permission.LOG_COMPAT_CHANGE" />
         <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.providers.contacts">
@@ -193,6 +209,7 @@
         <permission name="android.permission.USE_RESERVED_DISK"/>
         <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
         <permission name="android.permission.LOG_COMPAT_CHANGE" />
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.providers.downloads">
@@ -205,6 +222,7 @@
         <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
         <permission name="android.permission.UPDATE_DEVICE_STATS"/>
         <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.providers.telephony">
@@ -214,6 +232,7 @@
         <!-- Permissions required for reading and logging compat changes -->
         <permission name="android.permission.LOG_COMPAT_CHANGE" />
         <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.server.telecom">
@@ -229,11 +248,13 @@
         <permission name="android.permission.MODIFY_PHONE_STATE"/>
         <permission name="android.permission.STOP_APP_SWITCHES"/>
         <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.sharedstoragebackup">
         <permission name="android.permission.WRITE_MEDIA_STORAGE"/>
         <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.shell">
@@ -547,16 +568,19 @@
         <permission name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS" />
         <!-- Permission required for BinaryTransparencyService shell API and host test -->
         <permission name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" />
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
         <permission name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/>
         <permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.soundpicker">
         <permission name="android.permission.INTERACT_ACROSS_USERS" />
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.tv">
@@ -568,15 +592,18 @@
         <permission name="android.permission.READ_CONTENT_RATING_SYSTEMS"/>
         <permission name="com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA"/>
         <permission name="com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.vpndialogs">
         <permission name="android.permission.CONTROL_VPN"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.wallpaper.livepicker">
         <permission name="android.permission.SET_WALLPAPER_COMPONENT"/>
         <permission name="android.permission.BIND_WALLPAPER"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.wallpaper">
@@ -584,25 +611,30 @@
         <permission name="android.permission.BIND_WALLPAPER"/>
         <permission name="android.permission.CUSTOMIZE_SYSTEM_UI"/>
         <permission name="android.permission.SET_WALLPAPER_DIM_AMOUNT"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.dynsystem">
         <permission name="android.permission.REBOOT"/>
         <permission name="android.permission.MANAGE_DYNAMIC_SYSTEM"/>
         <permission name="android.permission.READ_OEM_UNLOCK_STATE"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
     <privapp-permissions package="com.android.settings">
         <permission name="android.permission.INSTALL_DYNAMIC_SYSTEM"/>
         <permission name="android.permission.BIND_CELL_BROADCAST_SERVICE"/>
         <permission name="android.permission.READ_SAFETY_CENTER_STATUS" />
         <permission name="android.permission.SEND_SAFETY_CENTER_UPDATE" />
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.bips">
         <permission name="android.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.calllogbackup">
         <permission name="com.android.voicemail.permission.READ_VOICEMAIL"/>
+        <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
     </privapp-permissions>
 </permissions>
diff --git a/graphics/java/android/graphics/text/LineBreaker.java b/graphics/java/android/graphics/text/LineBreaker.java
index 0e3fb16..9707126 100644
--- a/graphics/java/android/graphics/text/LineBreaker.java
+++ b/graphics/java/android/graphics/text/LineBreaker.java
@@ -17,6 +17,7 @@
 package android.graphics.text;
 
 import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
+import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
 
 import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
@@ -163,7 +164,8 @@
     /** @hide */
     @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = {
             JUSTIFICATION_MODE_NONE,
-            JUSTIFICATION_MODE_INTER_WORD
+            JUSTIFICATION_MODE_INTER_WORD,
+            JUSTIFICATION_MODE_INTER_CHARACTER,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface JustificationMode {}
@@ -179,6 +181,12 @@
     public static final int JUSTIFICATION_MODE_INTER_WORD = 1;
 
     /**
+     * Value for justification mode indicating the text is justified by stretching letter spacing.
+     */
+    @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+    public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2;
+
+    /**
      * Helper class for creating a {@link LineBreaker}.
      */
     public static final class Builder {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 83d555c..14a4677 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -17,6 +17,7 @@
 package androidx.window.extensions.embedding;
 
 import static android.app.ActivityManager.START_SUCCESS;
+import static android.app.ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -110,6 +111,10 @@
     static final boolean ENABLE_SHELL_TRANSITIONS =
             SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
 
+    // TODO(b/295993745): remove after prebuilt library is updated.
+    private static final String KEY_ACTIVITY_STACK_TOKEN =
+            "androidx.window.extensions.embedding.ActivityStackToken";
+
     @VisibleForTesting
     @GuardedBy("mLock")
     final SplitPresenter mPresenter;
@@ -2779,8 +2784,17 @@
             // TODO(b/232042367): Consolidate the activity create handling so that we can handle
             // cross-process the same as normal.
 
+            IBinder activityStackToken = options.getBinder(KEY_ACTIVITY_STACK_TOKEN);
+            if (activityStackToken != null) {
+                // Put activityStack token to #KEY_LAUNCH_TASK_FRAGMENT_TOKEN to launch the activity
+                // into the taskFragment associated with the token.
+                options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, activityStackToken);
+            }
+
             // Early return if the launching taskfragment is already been set.
-            if (options.getBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) {
+            // TODO(b/295993745): Use KEY_LAUNCH_TASK_FRAGMENT_TOKEN after WM Jetpack migrates to
+            // bundle. This is still needed to support #setLaunchingActivityStack.
+            if (options.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) {
                 synchronized (mLock) {
                     mCurrentIntent = intent;
                 }
@@ -2837,7 +2851,7 @@
                     // Amend the request to let the WM know that the activity should be placed in
                     // the dedicated container.
                     // TODO(b/229680885): skip override launching TaskFragment token by split-rule
-                    options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+                    options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
                             launchedInTaskFragment.getTaskFragmentToken());
                     mCurrentIntent = intent;
                 } else {
@@ -2855,8 +2869,7 @@
                 if (mCurrentIntent != null && result != START_SUCCESS) {
                     // Clear the pending appeared intent if the activity was not started
                     // successfully.
-                    final IBinder token = bOptions.getBinder(
-                            ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN);
+                    final IBinder token = bOptions.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN);
                     if (token != null) {
                         final TaskFragmentContainer container = getContainer(token);
                         if (container != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
index 30d5edb..160f922 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
@@ -199,6 +199,10 @@
     }
 
     private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) {
+        if (leash == null || !leash.isValid()) {
+            return;
+        }
+
         final float scale = targetRect.width() / mStartTaskRect.width();
         mTransformMatrix.reset();
         mTransformMatrix.setScale(scale, scale);
@@ -211,12 +215,16 @@
 
     private void finishAnimation() {
         if (mEnteringTarget != null) {
-            mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
-            mEnteringTarget.leash.release();
+            if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) {
+                mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
+                mEnteringTarget.leash.release();
+            }
             mEnteringTarget = null;
         }
         if (mClosingTarget != null) {
-            mClosingTarget.leash.release();
+            if (mClosingTarget.leash != null) {
+                mClosingTarget.leash.release();
+            }
             mClosingTarget = null;
         }
         if (mBackground != null) {
@@ -260,7 +268,9 @@
     }
 
     private void onGestureCommitted() {
-        if (mEnteringTarget == null || mClosingTarget == null) {
+        if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null
+                || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid()
+                || !mClosingTarget.leash.isValid()) {
             finishAnimation();
             return;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index ac2a1c8..adc7839 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -208,7 +208,9 @@
         float top = mapRange(progress, mClosingStartRect.top, targetTop);
         float width = mapRange(progress, mClosingStartRect.width(), targetWidth);
         float height = mapRange(progress, mClosingStartRect.height(), targetHeight);
-        mTransaction.setLayer(mClosingTarget.leash, 0);
+        if (mClosingTarget.leash != null && mClosingTarget.leash.isValid()) {
+            mTransaction.setLayer(mClosingTarget.leash, 0);
+        }
 
         mClosingCurrentRect.set(left, top, left + width, top + height);
         applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius);
@@ -226,7 +228,7 @@
 
     /** Transform the target window to match the target rect. */
     private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) {
-        if (leash == null) {
+        if (leash == null || !leash.isValid()) {
             return;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index d4a9289..a5f7880 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -137,7 +137,7 @@
  * The controller manages addition, removal, and visible state of bubbles on screen.
  */
 public class BubbleController implements ConfigurationChangeListener,
-        RemoteCallable<BubbleController> {
+        RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider {
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
 
@@ -706,6 +706,7 @@
         return mBubbleIconFactory;
     }
 
+    @Override
     public Bubbles.SysuiProxy getSysuiProxy() {
         return mSysuiProxy;
     }
@@ -732,8 +733,7 @@
             if (mStackView == null) {
                 mStackView = new BubbleStackView(
                         mContext, this, mBubbleData, mSurfaceSynchronizer,
-                        mFloatingContentCoordinator,
-                        mMainExecutor);
+                        mFloatingContentCoordinator, this, mMainExecutor);
                 mStackView.onOrientationChanged();
                 if (mExpandListener != null) {
                     mStackView.setExpandListener(mExpandListener);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index c25d412..a619401 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -206,6 +206,7 @@
             };
     private final BubbleController mBubbleController;
     private final BubbleData mBubbleData;
+    private final Bubbles.SysuiProxy.Provider mSysuiProxyProvider;
     private StackViewState mStackViewState = new StackViewState();
 
     private final ValueAnimator mDismissBubbleAnimator;
@@ -875,12 +876,14 @@
     public BubbleStackView(Context context, BubbleController bubbleController,
             BubbleData data, @Nullable SurfaceSynchronizer synchronizer,
             FloatingContentCoordinator floatingContentCoordinator,
+            Bubbles.SysuiProxy.Provider sysuiProxyProvider,
             ShellExecutor mainExecutor) {
         super(context);
 
         mMainExecutor = mainExecutor;
         mBubbleController = bubbleController;
         mBubbleData = data;
+        mSysuiProxyProvider = sysuiProxyProvider;
 
         Resources res = getResources();
         mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
@@ -2090,7 +2093,7 @@
 
         hideCurrentInputMethod();
 
-        mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand);
+        mSysuiProxyProvider.getSysuiProxy().onStackExpandChanged(shouldExpand);
 
         if (wasExpanded) {
             stopMonitoringSwipeUpGesture();
@@ -3034,7 +3037,7 @@
         if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
             mManageMenu.setVisibility(View.INVISIBLE);
             mManageMenuScrim.setVisibility(INVISIBLE);
-            mBubbleController.getSysuiProxy().onManageMenuExpandChanged(false /* show */);
+            mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(false /* show */);
             return;
         }
         if (show) {
@@ -3048,7 +3051,7 @@
             }
         };
 
-        mBubbleController.getSysuiProxy().onManageMenuExpandChanged(show);
+        mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(show);
         mManageMenuScrim.animate()
                 .setInterpolator(show ? ALPHA_IN : ALPHA_OUT)
                 .alpha(show ? BUBBLE_EXPANDED_SCRIM_ALPHA : 0f)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 759246e..28af0ca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -321,6 +321,13 @@
 
     /** Callback to tell SysUi components execute some methods. */
     interface SysuiProxy {
+
+        /** Provider interface for {@link SysuiProxy}. */
+        interface Provider {
+            /** Returns {@link SysuiProxy}. */
+            SysuiProxy getSysuiProxy();
+        }
+
         void isNotificationPanelExpand(Consumer<Boolean> callback);
 
         void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
index 9ce46d6..e9cd73b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
@@ -95,8 +95,8 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
-                + "entering PIP from an Activity Embedding window");
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for entering PIP from"
+                + " an Activity Embedding window #%d", info.getDebugId());
         // Split into two transitions (wct)
         TransitionInfo.Change pipChange = null;
         final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
@@ -146,6 +146,8 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for opening an intent"
+                + " with a remote transition and PIP #%d", info.getDebugId());
         boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip(
                 info, startTransaction, finishTransaction, finishCallback);
         // Consume the transition on remote handler if the leftover handler already handle this
@@ -192,8 +194,9 @@
             }
             return false;
         }
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
-                + " animation because remote-animation likely doesn't support it");
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate"
+                + " animation because remote-animation likely doesn't support it #%d",
+                info.getDebugId());
         // Split the transition into 2 parts: the pip part and the rest.
         mInFlightSubAnimations = 2;
         // make a new startTransaction because pip's startEnterAnimation "consumes" it so
@@ -218,6 +221,9 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for unfolding #%d",
+                info.getDebugId());
+
         final Transitions.TransitionFinishCallback finishCB = (wct) -> {
             mInFlightSubAnimations--;
             if (mInFlightSubAnimations > 0) return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
index 643e026..4ea71490 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -30,9 +30,11 @@
 import android.window.TransitionInfo;
 import android.window.WindowContainerTransaction;
 
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
 import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
 import com.android.wm.shell.splitscreen.StageCoordinator;
 
@@ -77,6 +79,9 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition for Recents during"
+                + " Desktop #%d", info.getDebugId());
+
         if (mInfo == null) {
             mInfo = info;
             mFinishT = finishTransaction;
@@ -109,6 +114,9 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for Recents during"
+                + " Keyguard #%d", info.getDebugId());
+
         if (mInfo == null) {
             mInfo = info;
             mFinishT = finishTransaction;
@@ -122,6 +130,9 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for Recents during"
+                + " split screen #%d", info.getDebugId());
+
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
             // Pip auto-entering info might be appended to recent transition like pressing
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index f583321..4878df8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -125,7 +125,7 @@
                 mock<BubbleProperties>())
 
         bubbleStackView = BubbleStackView(context, bubbleController, bubbleData,
-                surfaceSynchronizer, FloatingContentCoordinator(), mainExecutor)
+                surfaceSynchronizer, FloatingContentCoordinator(), bubbleController, mainExecutor)
         bubbleBarLayerView = BubbleBarLayerView(context, bubbleController)
     }
 
diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp
index 4d020c5..5f5ffe9 100644
--- a/libs/hwui/AutoBackendTextureRelease.cpp
+++ b/libs/hwui/AutoBackendTextureRelease.cpp
@@ -21,6 +21,7 @@
 #include <include/gpu/GrDirectContext.h>
 #include <include/gpu/GrBackendSurface.h>
 #include <include/gpu/MutableTextureState.h>
+#include <include/gpu/vk/VulkanMutableTextureState.h>
 #include "renderthread/RenderThread.h"
 #include "utils/Color.h"
 #include "utils/PaintUtils.h"
@@ -142,8 +143,9 @@
     LOG_ALWAYS_FATAL_IF(Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan);
     if (mBackendTexture.isValid()) {
         // Passing in VK_IMAGE_LAYOUT_UNDEFINED means we keep the old layout.
-        skgpu::MutableTextureState newState(VK_IMAGE_LAYOUT_UNDEFINED,
-                                            VK_QUEUE_FAMILY_FOREIGN_EXT);
+        skgpu::MutableTextureState newState = skgpu::MutableTextureStates::MakeVulkan(
+                                                                  VK_IMAGE_LAYOUT_UNDEFINED,
+                                                                  VK_QUEUE_FAMILY_FOREIGN_EXT);
 
         // The unref for this ref happens in the releaseProc passed into setBackendTextureState. The
         // releaseProc callback will be made when the work to set the new state has finished on the
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 0b42c88..f526a28 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -230,7 +230,7 @@
  * stencil buffer may be needed. Views that use a functor to draw will be forced onto a layer.
  */
 void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) {
-    if (mDamageGenerationId == info.damageGenerationId) {
+    if (mDamageGenerationId == info.damageGenerationId && mDamageGenerationId != 0) {
         // We hit the same node a second time in the same tree. We don't know the minimal
         // damage rect anymore, so just push the biggest we can onto our parent's transform
         // We push directly onto parent in case we are clipped to bounds but have moved position.
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index 1f3834be..c904542 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -262,7 +262,7 @@
     DisplayList mDisplayList;
     DisplayList mStagingDisplayList;
 
-    int64_t mDamageGenerationId;
+    int64_t mDamageGenerationId = 0;
 
     friend class AnimatorManager;
     AnimatorManager mAnimatorManager;
diff --git a/libs/hwui/jni/PathMeasure.cpp b/libs/hwui/jni/PathMeasure.cpp
index acf893e..79acb6c 100644
--- a/libs/hwui/jni/PathMeasure.cpp
+++ b/libs/hwui/jni/PathMeasure.cpp
@@ -17,7 +17,11 @@
 
 #include "GraphicsJNI.h"
 
+#include "SkMatrix.h"
+#include "SkPath.h"
 #include "SkPathMeasure.h"
+#include "SkPoint.h"
+#include "SkScalar.h"
 
 /*  We declare an explicit pair, so that we don't have to rely on the java
     client to be sure not to edit the path while we have an active measure
diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/gnss.aconfig
index 5f1279e..8c7c871 100644
--- a/location/java/android/location/flags/gnss.aconfig
+++ b/location/java/android/location/flags/gnss.aconfig
@@ -36,15 +36,15 @@
 }
 
 flag {
-    name: "gnss_configuration_from_resource"
-    namespace: "location"
-    description: "Flag for GNSS configuration from resource"
-    bug: "317734846"
-}
-
-flag {
     name: "replace_future_elapsed_realtime_jni"
     namespace: "location"
     description: "Flag for replacing future elapsedRealtime in JNI"
     bug: "314328533"
 }
+
+flag {
+    name: "gnss_configuration_from_resource"
+    namespace: "location"
+    description: "Flag for GNSS configuration from resource"
+    bug: "317734846"
+}
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 2eec9b3..8dfa6be 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -754,15 +754,16 @@
 
     void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);
 
-    oneway void startLoudnessCodecUpdates(int piid, in List<LoudnessCodecInfo> codecInfoSet);
+    oneway void startLoudnessCodecUpdates(int sessionId);
 
-    oneway void stopLoudnessCodecUpdates(int piid);
+    oneway void stopLoudnessCodecUpdates(int sessionId);
 
-    oneway void addLoudnessCodecInfo(int piid, int mediaCodecHash, in LoudnessCodecInfo codecInfo);
+    oneway void addLoudnessCodecInfo(int sessionId, int mediaCodecHash,
+            in LoudnessCodecInfo codecInfo);
 
-    oneway void removeLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
+    oneway void removeLoudnessCodecInfo(int sessionId, in LoudnessCodecInfo codecInfo);
 
-    PersistableBundle getLoudnessParams(int piid, in LoudnessCodecInfo codecInfo);
+    PersistableBundle getLoudnessParams(in LoudnessCodecInfo codecInfo);
 
     @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
diff --git a/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl b/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl
index 16eaaea..2022427 100644
--- a/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl
+++ b/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl
@@ -16,6 +16,7 @@
 
 package android.media;
 
+import android.media.AudioAttributes;
 import android.os.PersistableBundle;
 
 /**
@@ -26,6 +27,6 @@
  */
 oneway interface ILoudnessCodecUpdatesDispatcher {
 
-    void dispatchLoudnessCodecParameterChange(int piid, in PersistableBundle params);
+    void dispatchLoudnessCodecParameterChange(int sessionId, in PersistableBundle params);
 
 }
\ No newline at end of file
diff --git a/media/java/android/media/LoudnessCodecConfigurator.java b/media/java/android/media/LoudnessCodecConfigurator.java
deleted file mode 100644
index aadd783..0000000
--- a/media/java/android/media/LoudnessCodecConfigurator.java
+++ /dev/null
@@ -1,431 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.media;
-
-import static android.media.AudioPlaybackConfiguration.PLAYER_PIID_INVALID;
-import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
-import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
-import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
-
-import android.annotation.CallbackExecutor;
-import android.annotation.FlaggedApi;
-import android.os.Bundle;
-import android.util.Log;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Class for getting recommended loudness parameter updates for audio decoders, according to the
- * encoded format and current audio routing. Those updates can be automatically applied to the
- * {@link MediaCodec} instance(s), or be provided to the user. The codec loudness management
- * parameter updates are defined by the CTA-2075 standard.
- * <p>A new object should be instantiated for each {@link AudioTrack} with the help
- * of {@link #create()} or {@link #create(Executor, OnLoudnessCodecUpdateListener)}.
- */
-@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-public class LoudnessCodecConfigurator {
-    private static final String TAG = "LoudnessCodecConfigurator";
-
-    /**
-     * Listener used for receiving asynchronous loudness metadata updates.
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public interface OnLoudnessCodecUpdateListener {
-        /**
-         * Contains the MediaCodec key/values that can be set directly to
-         * configure the loudness of the handle's corresponding decoder (see
-         * {@link MediaCodec#setParameters(Bundle)}).
-         *
-         * @param mediaCodec  the mediaCodec that will receive the new parameters
-         * @param codecValues contains loudness key/value pairs that can be set
-         *                    directly on the mediaCodec. The listener can modify
-         *                    these values with their own edits which will be
-         *                    returned for the mediaCodec configuration
-         * @return a Bundle which contains the original computed codecValues
-         * aggregated with user edits. The platform will configure the associated
-         * MediaCodecs with the returned Bundle params.
-         */
-        @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-        @NonNull
-        default Bundle onLoudnessCodecUpdate(@NonNull MediaCodec mediaCodec,
-                                             @NonNull Bundle codecValues) {
-            return codecValues;
-        }
-    }
-
-    @NonNull private final LoudnessCodecDispatcher mLcDispatcher;
-
-    private final Object mConfiguratorLock = new Object();
-
-    @GuardedBy("mConfiguratorLock")
-    private AudioTrack mAudioTrack;
-
-    @GuardedBy("mConfiguratorLock")
-    private final Executor mExecutor;
-
-    @GuardedBy("mConfiguratorLock")
-    private final OnLoudnessCodecUpdateListener mListener;
-
-    @GuardedBy("mConfiguratorLock")
-    private final HashMap<LoudnessCodecInfo, Set<MediaCodec>> mMediaCodecs = new HashMap<>();
-
-    /**
-     * Creates a new instance of {@link LoudnessCodecConfigurator}
-     *
-     * <p>This method should be used when the client does not need to alter the
-     * codec loudness parameters before they are applied to the audio decoders.
-     * Otherwise, use {@link #create(Executor, OnLoudnessCodecUpdateListener)}.
-     *
-     * @return the {@link LoudnessCodecConfigurator} instance
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public static @NonNull LoudnessCodecConfigurator create() {
-        return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
-                Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
-    }
-
-    /**
-     * Creates a new instance of {@link LoudnessCodecConfigurator}
-     *
-     * <p>This method should be used when the client wants to alter the codec
-     * loudness parameters before they are applied to the audio decoders.
-     * Otherwise, use {@link #create()}.
-     *
-     * @param executor {@link Executor} to handle the callbacks
-     * @param listener used for receiving updates
-     *
-     * @return the {@link LoudnessCodecConfigurator} instance
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public static @NonNull LoudnessCodecConfigurator create(
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnLoudnessCodecUpdateListener listener) {
-        Objects.requireNonNull(executor, "Executor cannot be null");
-        Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
-
-        return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
-                executor, listener);
-    }
-
-    /**
-     * Creates a new instance of {@link LoudnessCodecConfigurator}
-     *
-     * <p>This method should be used only in testing
-     *
-     * @param service interface for communicating with AudioService
-     * @param executor {@link Executor} to handle the callbacks
-     * @param listener used for receiving updates
-     *
-     * @return the {@link LoudnessCodecConfigurator} instance
-     *
-     * @hide
-     */
-    public static @NonNull LoudnessCodecConfigurator createForTesting(
-            @NonNull IAudioService service,
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnLoudnessCodecUpdateListener listener) {
-        Objects.requireNonNull(service, "IAudioService cannot be null");
-        Objects.requireNonNull(executor, "Executor cannot be null");
-        Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
-
-        return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(service),
-                executor, listener);
-    }
-
-    /** @hide */
-    private LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher,
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnLoudnessCodecUpdateListener listener) {
-        mLcDispatcher = Objects.requireNonNull(lcDispatcher, "Dispatcher cannot be null");
-        mExecutor = Objects.requireNonNull(executor, "Executor cannot be null");
-        mListener = Objects.requireNonNull(listener,
-                "OnLoudnessCodecUpdateListener cannot be null");
-    }
-
-    /**
-     * Sets the {@link AudioTrack} and starts receiving asynchronous updates for
-     * the registered {@link MediaCodec}s (see {@link #addMediaCodec(MediaCodec)})
-     *
-     * <p>The AudioTrack should be the one that receives audio data from the
-     * added audio decoders and is used to determine the device routing on which
-     * the audio streaming will take place. This will directly influence the
-     * loudness parameters.
-     * <p>After calling this method the framework will compute the initial set of
-     * parameters which will be applied to the registered codecs/returned to the
-     * listener for modification.
-     *
-     * @param audioTrack the track that will receive audio data from the provided
-     *                   audio decoders. In case this is {@code null} this
-     *                   method will have the effect of clearing the existing set
-     *                   {@link AudioTrack} and will stop receiving asynchronous
-     *                   loudness updates
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void setAudioTrack(@Nullable AudioTrack audioTrack) {
-        List<LoudnessCodecInfo> codecInfos;
-        int piid = PLAYER_PIID_INVALID;
-        int oldPiid = PLAYER_PIID_INVALID;
-        synchronized (mConfiguratorLock) {
-            if (mAudioTrack != null && mAudioTrack == audioTrack) {
-                Log.v(TAG, "Loudness configurator already started for piid: "
-                        + mAudioTrack.getPlayerIId());
-                return;
-            }
-
-            codecInfos = getLoudnessCodecInfoList_l();
-            if (mAudioTrack != null) {
-                oldPiid = mAudioTrack.getPlayerIId();
-                mLcDispatcher.removeLoudnessCodecListener(this);
-            }
-            if (audioTrack != null) {
-                piid = audioTrack.getPlayerIId();
-                mLcDispatcher.addLoudnessCodecListener(this, mExecutor, mListener);
-            }
-
-            mAudioTrack = audioTrack;
-        }
-
-        if (oldPiid != PLAYER_PIID_INVALID) {
-            Log.v(TAG, "Loudness configurator stopping updates for piid: " + oldPiid);
-            mLcDispatcher.stopLoudnessCodecUpdates(oldPiid);
-        }
-        if (piid != PLAYER_PIID_INVALID) {
-            Log.v(TAG, "Loudness configurator starting updates for piid: " + piid);
-            mLcDispatcher.startLoudnessCodecUpdates(piid, codecInfos);
-        }
-    }
-
-    /**
-     * Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack}
-     * which the client sets
-     * (see {@link LoudnessCodecConfigurator#setAudioTrack(AudioTrack)}).
-     *
-     * <p>This method can be called while asynchronous updates are live.
-     *
-     * <p>No new element will be added if the passed {@code mediaCodec} was
-     * previously added.
-     *
-     * @param mediaCodec the codec to start receiving asynchronous loudness
-     *                   updates. The codec has to be in a configured or started
-     *                   state in order to add it for loudness updates.
-     * @throws IllegalArgumentException if the same {@code mediaCodec} was already
-     *                                  added before.
-     * @return {@code false} if the {@code mediaCodec} was not configured or does
-     *         not contain loudness metadata, {@code true} otherwise.
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public boolean addMediaCodec(@NonNull MediaCodec mediaCodec) {
-        final MediaCodec mc = Objects.requireNonNull(mediaCodec,
-                "MediaCodec for addMediaCodec cannot be null");
-        int piid = PLAYER_PIID_INVALID;
-        final LoudnessCodecInfo mcInfo = getCodecInfo(mc);
-
-        if (mcInfo == null) {
-            Log.v(TAG, "Could not extract codec loudness information");
-            return false;
-        }
-        synchronized (mConfiguratorLock) {
-            final AtomicBoolean containsCodec = new AtomicBoolean(false);
-            Set<MediaCodec> newSet = mMediaCodecs.computeIfPresent(mcInfo, (info, codecSet) -> {
-                containsCodec.set(!codecSet.add(mc));
-                return codecSet;
-            });
-            if (newSet == null) {
-                newSet = new HashSet<>();
-                newSet.add(mc);
-                mMediaCodecs.put(mcInfo, newSet);
-            }
-            if (containsCodec.get()) {
-                throw new IllegalArgumentException(
-                        "Loudness configurator already added " + mediaCodec);
-            }
-            if (mAudioTrack != null) {
-                piid = mAudioTrack.getPlayerIId();
-            }
-        }
-
-        if (piid != PLAYER_PIID_INVALID) {
-            mLcDispatcher.addLoudnessCodecInfo(piid, mediaCodec.hashCode(), mcInfo);
-        }
-
-        return true;
-    }
-
-    /**
-     * Removes the {@link MediaCodec} from receiving loudness updates.
-     *
-     * <p>This method can be called while asynchronous updates are live.
-     *
-     * <p>No elements will be removed if the passed mediaCodec was not added before.
-     *
-     * @param mediaCodec the element to remove for receiving asynchronous updates
-     * @throws IllegalArgumentException if the {@code mediaCodec} was not configured,
-     *                                  does not contain loudness metadata or if it
-     *                                  was not added before
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
-        int piid = PLAYER_PIID_INVALID;
-        LoudnessCodecInfo mcInfo;
-        AtomicBoolean removedMc = new AtomicBoolean(false);
-        AtomicBoolean removeInfo = new AtomicBoolean(false);
-
-        mcInfo = getCodecInfo(Objects.requireNonNull(mediaCodec,
-                "MediaCodec for removeMediaCodec cannot be null"));
-
-        if (mcInfo == null) {
-            throw new IllegalArgumentException("Could not extract codec loudness information");
-        }
-        synchronized (mConfiguratorLock) {
-            if (mAudioTrack != null) {
-                piid = mAudioTrack.getPlayerIId();
-            }
-            mMediaCodecs.computeIfPresent(mcInfo, (format, mcs) -> {
-                removedMc.set(mcs.remove(mediaCodec));
-                if (mcs.isEmpty()) {
-                    // remove the entry
-                    removeInfo.set(true);
-                    return null;
-                }
-                return mcs;
-            });
-            if (!removedMc.get()) {
-                throw new IllegalArgumentException(
-                        "Loudness configurator does not contain " + mediaCodec);
-            }
-        }
-
-        if (piid != PLAYER_PIID_INVALID && removeInfo.get()) {
-            mLcDispatcher.removeLoudnessCodecInfo(piid, mcInfo);
-        }
-    }
-
-    /**
-     * Gets synchronous loudness updates when no listener is required. The provided
-     * {@link MediaCodec} streams audio data to the passed {@link AudioTrack}.
-     *
-     * @param audioTrack track that receives audio data from the passed
-     *                   {@link MediaCodec}
-     * @param mediaCodec codec that decodes loudness annotated data for the passed
-     *                   {@link AudioTrack}
-     *
-     * @return the {@link Bundle} containing the current loudness parameters. Caller is
-     * responsible to update the {@link MediaCodec}
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    @NonNull
-    public Bundle getLoudnessCodecParams(@NonNull AudioTrack audioTrack,
-            @NonNull MediaCodec mediaCodec) {
-        Objects.requireNonNull(audioTrack, "Passed audio track cannot be null");
-
-        LoudnessCodecInfo codecInfo = getCodecInfo(mediaCodec);
-        if (codecInfo == null) {
-            return new Bundle();
-        }
-
-        return mLcDispatcher.getLoudnessCodecParams(audioTrack.getPlayerIId(), codecInfo);
-    }
-
-    /** @hide */
-    /*package*/ int getAssignedTrackPiid() {
-        int piid = PLAYER_PIID_INVALID;
-
-        synchronized (mConfiguratorLock) {
-            if (mAudioTrack == null) {
-                return piid;
-            }
-            piid = mAudioTrack.getPlayerIId();
-        }
-
-        return piid;
-    }
-
-    /** @hide */
-    /*package*/ Map<LoudnessCodecInfo, Set<MediaCodec>> getRegisteredMediaCodecs() {
-        synchronized (mConfiguratorLock) {
-            return mMediaCodecs;
-        }
-    }
-
-    @GuardedBy("mConfiguratorLock")
-    private List<LoudnessCodecInfo> getLoudnessCodecInfoList_l() {
-        return mMediaCodecs.values().stream().flatMap(listMc -> listMc.stream().map(
-                LoudnessCodecConfigurator::getCodecInfo)).toList();
-    }
-
-    @Nullable
-    private static LoudnessCodecInfo getCodecInfo(@NonNull MediaCodec mediaCodec) {
-        LoudnessCodecInfo lci = new LoudnessCodecInfo();
-        final MediaCodecInfo codecInfo = mediaCodec.getCodecInfo();
-        if (codecInfo.isEncoder()) {
-            // loudness info only for decoders
-            Log.w(TAG, "MediaCodec used for encoding does not support loudness annotation");
-            return null;
-        }
-
-        try {
-            final MediaFormat inputFormat = mediaCodec.getInputFormat();
-            final String mimeType = inputFormat.getString(MediaFormat.KEY_MIME);
-            if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mimeType)) {
-                // check both KEY_AAC_PROFILE and KEY_PROFILE as some codecs may only recognize
-                // one of these two keys
-                int aacProfile = -1;
-                int profile = -1;
-                try {
-                    aacProfile = inputFormat.getInteger(MediaFormat.KEY_AAC_PROFILE);
-                } catch (NullPointerException e) {
-                    // does not contain KEY_AAC_PROFILE. do nothing
-                }
-                try {
-                    profile = inputFormat.getInteger(MediaFormat.KEY_PROFILE);
-                } catch (NullPointerException e) {
-                    // does not contain KEY_PROFILE. do nothing
-                }
-                if (aacProfile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE
-                        || profile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE) {
-                    lci.metadataType = CODEC_METADATA_TYPE_MPEG_D;
-                } else {
-                    lci.metadataType = CODEC_METADATA_TYPE_MPEG_4;
-                }
-            } else {
-                Log.w(TAG, "MediaCodec mime type not supported for loudness annotation");
-                return null;
-            }
-
-            final MediaFormat outputFormat = mediaCodec.getOutputFormat();
-            lci.isDownmixing = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
-                    < inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
-        } catch (IllegalStateException e) {
-            Log.e(TAG, "MediaCodec is not configured", e);
-            return null;
-        }
-
-        return lci;
-    }
-}
diff --git a/media/java/android/media/LoudnessCodecController.java b/media/java/android/media/LoudnessCodecController.java
new file mode 100644
index 0000000..b3e5c52
--- /dev/null
+++ b/media/java/android/media/LoudnessCodecController.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
+import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.media.permission.SafeCloseable;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Class for getting recommended loudness parameter updates for audio decoders as they are used
+ * to play back media content according to the encoded format and current audio routing. These
+ * audio decoder updates leverage loudness metadata present in compressed audio streams. They
+ * ensure the loudness and dynamic range of the content is optimized to the physical
+ * characteristics of the audio output device (e.g. phone microspeakers vs headphones vs TV
+ * speakers).Those updates can be automatically applied to the {@link MediaCodec} instance(s), or
+ * be provided to the user. The codec loudness management parameter updates are computed in
+ * accordance to the CTA-2075 standard.
+ * <p>A new object should be instantiated for each audio session
+ * (see {@link AudioManager#generateAudioSessionId()}) using creator methods {@link #create(int)} or
+ * {@link #create(int, Executor, OnLoudnessCodecUpdateListener)}.
+ */
+@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+public class LoudnessCodecController implements SafeCloseable {
+    private static final String TAG = "LoudnessCodecController";
+
+    /**
+     * Listener used for receiving asynchronous loudness metadata updates.
+     */
+    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public interface OnLoudnessCodecUpdateListener {
+        /**
+         * Contains the MediaCodec key/values that can be set directly to
+         * configure the loudness of the handle's corresponding decoder (see
+         * {@link MediaCodec#setParameters(Bundle)}).
+         *
+         * @param mediaCodec  the mediaCodec that will receive the new parameters
+         * @param codecValues contains loudness key/value pairs that can be set
+         *                    directly on the mediaCodec. The listener can modify
+         *                    these values with their own edits which will be
+         *                    returned for the mediaCodec configuration
+         *
+         * @return a Bundle which contains the original computed codecValues
+         * aggregated with user edits. The platform will configure the associated
+         * MediaCodecs with the returned Bundle params.
+         */
+        @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+        @NonNull
+        default Bundle onLoudnessCodecUpdate(@NonNull MediaCodec mediaCodec,
+                @NonNull Bundle codecValues) {
+            return codecValues;
+        }
+    }
+
+    @NonNull
+    private final LoudnessCodecDispatcher mLcDispatcher;
+
+    private final Object mControllerLock = new Object();
+
+    private final int mSessionId;
+
+    @GuardedBy("mControllerLock")
+    private final HashMap<LoudnessCodecInfo, Set<MediaCodec>> mMediaCodecs = new HashMap<>();
+
+    /**
+     * Creates a new instance of {@link LoudnessCodecController}
+     *
+     * <p>This method should be used when the client does not need to alter the
+     * codec loudness parameters before they are applied to the audio decoders.
+     * Otherwise, use {@link #create(int, Executor, OnLoudnessCodecUpdateListener)}.
+     *
+     * @param sessionId  the session ID of the track that will receive data
+     *                        from the added {@link MediaCodec}'s
+     *
+     * @return the {@link LoudnessCodecController} instance
+     */
+    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public static @NonNull LoudnessCodecController create(int sessionId) {
+        final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(
+                AudioManager.getService());
+        final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
+                sessionId);
+        dispatcher.addLoudnessCodecListener(controller, Executors.newSingleThreadExecutor(),
+                new OnLoudnessCodecUpdateListener() {});
+        dispatcher.startLoudnessCodecUpdates(sessionId);
+        return controller;
+    }
+
+    /**
+     * Creates a new instance of {@link LoudnessCodecController}
+     *
+     * <p>This method should be used when the client wants to alter the codec
+     * loudness parameters before they are applied to the audio decoders.
+     * Otherwise, use {@link #create( int)}.
+     *
+     * @param sessionId       the session ID of the track that will receive data
+     *                        from the added {@link MediaCodec}'s
+     * @param executor        {@link Executor} to handle the callbacks
+     * @param listener        used for receiving updates
+     *
+     * @return the {@link LoudnessCodecController} instance
+     */
+    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public static @NonNull LoudnessCodecController create(
+            int sessionId,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnLoudnessCodecUpdateListener listener) {
+        Objects.requireNonNull(executor, "Executor cannot be null");
+        Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
+
+        final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(
+                AudioManager.getService());
+        final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
+                sessionId);
+        dispatcher.addLoudnessCodecListener(controller, executor, listener);
+        dispatcher.startLoudnessCodecUpdates(sessionId);
+        return controller;
+    }
+
+    /**
+     * Creates a new instance of {@link LoudnessCodecController}
+     *
+     * <p>This method should be used only in testing
+     *
+     * @param sessionId  the session ID of the track that will receive data
+     *                        from the added {@link MediaCodec}'s
+     * @param executor {@link Executor} to handle the callbacks
+     * @param listener used for receiving updates
+     * @param service  interface for communicating with AudioService
+     *
+     * @return the {@link LoudnessCodecController} instance
+     * @hide
+     */
+    public static @NonNull LoudnessCodecController createForTesting(
+            int sessionId,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnLoudnessCodecUpdateListener listener,
+            @NonNull IAudioService service) {
+        Objects.requireNonNull(service, "IAudioService cannot be null");
+        Objects.requireNonNull(executor, "Executor cannot be null");
+        Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
+
+        final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(service);
+        final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
+                sessionId);
+        dispatcher.addLoudnessCodecListener(controller, executor, listener);
+        dispatcher.startLoudnessCodecUpdates(sessionId);
+        return controller;
+    }
+
+    /** @hide */
+    private LoudnessCodecController(@NonNull LoudnessCodecDispatcher lcDispatcher, int sessionId) {
+        mLcDispatcher = Objects.requireNonNull(lcDispatcher, "Dispatcher cannot be null");
+        mSessionId = sessionId;
+    }
+
+    /**
+     * Adds a new {@link MediaCodec} that will stream data to a player
+     * which uses {@link #mSessionId}.
+     *
+     * <p>No new element will be added if the passed {@code mediaCodec} was
+     * previously added.
+     *
+     * @param mediaCodec the codec to start receiving asynchronous loudness
+     *                   updates. The codec has to be in a configured or started
+     *                   state in order to add it for loudness updates.
+     * @return {@code false} if the {@code mediaCodec} was not configured or does
+     * not contain loudness metadata, {@code true} otherwise.
+     * @throws IllegalArgumentException if the same {@code mediaCodec} was already
+     *                                  added before.
+     */
+    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public boolean addMediaCodec(@NonNull MediaCodec mediaCodec) {
+        final MediaCodec mc = Objects.requireNonNull(mediaCodec,
+                "MediaCodec for addMediaCodec cannot be null");
+        final LoudnessCodecInfo mcInfo = getCodecInfo(mc);
+
+        if (mcInfo == null) {
+            Log.v(TAG, "Could not extract codec loudness information");
+            return false;
+        }
+        synchronized (mControllerLock) {
+            final AtomicBoolean containsCodec = new AtomicBoolean(false);
+            Set<MediaCodec> newSet = mMediaCodecs.computeIfPresent(mcInfo, (info, codecSet) -> {
+                containsCodec.set(!codecSet.add(mc));
+                return codecSet;
+            });
+            if (newSet == null) {
+                newSet = new HashSet<>();
+                newSet.add(mc);
+                mMediaCodecs.put(mcInfo, newSet);
+            }
+            if (containsCodec.get()) {
+                throw new IllegalArgumentException(
+                        "Loudness controller already added " + mediaCodec);
+            }
+        }
+
+        mLcDispatcher.addLoudnessCodecInfo(mSessionId, mediaCodec.hashCode(),
+                mcInfo);
+
+        return true;
+    }
+
+    /**
+     * Removes the {@link MediaCodec} from receiving loudness updates.
+     *
+     * <p>This method can be called while asynchronous updates are live.
+     *
+     * <p>No elements will be removed if the passed mediaCodec was not added before.
+     *
+     * @param mediaCodec the element to remove for receiving asynchronous updates
+     * @throws IllegalArgumentException if the {@code mediaCodec} was not configured,
+     *                                  does not contain loudness metadata or if it
+     *                                  was not added before
+     */
+    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
+        LoudnessCodecInfo mcInfo;
+        AtomicBoolean removedMc = new AtomicBoolean(false);
+        AtomicBoolean removeInfo = new AtomicBoolean(false);
+
+        mcInfo = getCodecInfo(Objects.requireNonNull(mediaCodec,
+                "MediaCodec for removeMediaCodec cannot be null"));
+
+        if (mcInfo == null) {
+            throw new IllegalArgumentException("Could not extract codec loudness information");
+        }
+        synchronized (mControllerLock) {
+            mMediaCodecs.computeIfPresent(mcInfo, (format, mcs) -> {
+                removedMc.set(mcs.remove(mediaCodec));
+                if (mcs.isEmpty()) {
+                    // remove the entry
+                    removeInfo.set(true);
+                    return null;
+                }
+                return mcs;
+            });
+            if (!removedMc.get()) {
+                throw new IllegalArgumentException(
+                        "Loudness controller does not contain " + mediaCodec);
+            }
+        }
+
+        if (removeInfo.get()) {
+            mLcDispatcher.removeLoudnessCodecInfo(mSessionId, mcInfo);
+        }
+    }
+
+    /**
+     * Returns the loudness parameters of the registered audio decoders
+     *
+     * <p>Those parameters may have been automatically applied if the
+     * {@code LoudnessCodecController} was created with {@link #create(int)}, or they are the
+     * parameters that have been sent to the {@link OnLoudnessCodecUpdateListener} if using a
+     * codec update listener.
+     *
+     * @param mediaCodec codec that decodes loudness annotated data. Has to be added
+     *                   with {@link #addMediaCodec(MediaCodec)} before calling this
+     *                   method
+     * @throws IllegalArgumentException if the passed {@link MediaCodec} was not
+     *                                  added before calling this method
+     *
+     * @return the {@link Bundle} containing the current loudness parameters.
+     */
+    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+    @NonNull
+    public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) {
+        Objects.requireNonNull(mediaCodec, "MediaCodec cannot be null");
+
+        LoudnessCodecInfo codecInfo = getCodecInfo(mediaCodec);
+        if (codecInfo == null) {
+            throw new IllegalArgumentException("MediaCodec does not have valid codec information");
+        }
+
+        synchronized (mControllerLock) {
+            final Set<MediaCodec> codecs = mMediaCodecs.get(codecInfo);
+            if (codecs == null || !codecs.contains(mediaCodec)) {
+                throw new IllegalArgumentException(
+                        "MediaCodec was not added for loudness annotation");
+            }
+        }
+
+        return mLcDispatcher.getLoudnessCodecParams(codecInfo);
+    }
+
+    /**
+     * Stops any loudness updates and frees up the resources.
+     */
+    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void release() {
+        close();
+    }
+
+    /** @hide */
+    @Override
+    public void close() {
+        synchronized (mControllerLock) {
+            mMediaCodecs.clear();
+        }
+        mLcDispatcher.stopLoudnessCodecUpdates(mSessionId);
+    }
+
+    /** @hide */
+    /*package*/ int getSessionId() {
+        return mSessionId;
+    }
+
+    /** @hide */
+    /*package*/ Map<LoudnessCodecInfo, Set<MediaCodec>> getRegisteredMediaCodecs() {
+        synchronized (mControllerLock) {
+            return mMediaCodecs;
+        }
+    }
+
+    @Nullable
+    private static LoudnessCodecInfo getCodecInfo(@NonNull MediaCodec mediaCodec) {
+        LoudnessCodecInfo lci = new LoudnessCodecInfo();
+        final MediaCodecInfo codecInfo = mediaCodec.getCodecInfo();
+        if (codecInfo.isEncoder()) {
+            // loudness info only for decoders
+            Log.w(TAG, "MediaCodec used for encoding does not support loudness annotation");
+            return null;
+        }
+
+        try {
+            final MediaFormat inputFormat = mediaCodec.getInputFormat();
+            final String mimeType = inputFormat.getString(MediaFormat.KEY_MIME);
+            if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mimeType)) {
+                // check both KEY_AAC_PROFILE and KEY_PROFILE as some codecs may only recognize
+                // one of these two keys
+                int aacProfile = -1;
+                int profile = -1;
+                try {
+                    aacProfile = inputFormat.getInteger(MediaFormat.KEY_AAC_PROFILE);
+                } catch (NullPointerException e) {
+                    // does not contain KEY_AAC_PROFILE. do nothing
+                }
+                try {
+                    profile = inputFormat.getInteger(MediaFormat.KEY_PROFILE);
+                } catch (NullPointerException e) {
+                    // does not contain KEY_PROFILE. do nothing
+                }
+                if (aacProfile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE
+                        || profile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE) {
+                    lci.metadataType = CODEC_METADATA_TYPE_MPEG_D;
+                } else {
+                    lci.metadataType = CODEC_METADATA_TYPE_MPEG_4;
+                }
+            } else {
+                Log.w(TAG, "MediaCodec mime type not supported for loudness annotation");
+                return null;
+            }
+
+            final MediaFormat outputFormat = mediaCodec.getOutputFormat();
+            lci.isDownmixing = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
+                    < inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+        } catch (IllegalStateException e) {
+            Log.e(TAG, "MediaCodec is not configured", e);
+            return null;
+        }
+
+        return lci;
+    }
+}
diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java
index b546a81..46be54b 100644
--- a/media/java/android/media/LoudnessCodecDispatcher.java
+++ b/media/java/android/media/LoudnessCodecDispatcher.java
@@ -21,7 +21,7 @@
 import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL;
 
 import android.annotation.CallbackExecutor;
-import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
+import android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener;
 import android.os.Bundle;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
@@ -32,7 +32,6 @@
 
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
@@ -59,7 +58,7 @@
         private final Object mLock = new Object();
 
         @GuardedBy("mLock")
-        private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
+        private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecController>
                 mConfiguratorListener = new HashMap<>();
 
         public static synchronized LoudnessCodecUpdatesDispatcherStub getInstance() {
@@ -72,16 +71,16 @@
         private LoudnessCodecUpdatesDispatcherStub() {}
 
         @Override
-        public void dispatchLoudnessCodecParameterChange(int piid, PersistableBundle params) {
+        public void dispatchLoudnessCodecParameterChange(int sessionId, PersistableBundle params) {
             if (DEBUG) {
-                Log.d(TAG, "dispatchLoudnessCodecParameterChange for piid " + piid
+                Log.d(TAG, "dispatchLoudnessCodecParameterChange for sessionId " + sessionId
                         + " persistable bundle: " + params);
             }
             mLoudnessListenerMgr.callListeners(listener -> {
                 synchronized (mLock) {
                     mConfiguratorListener.computeIfPresent(listener, (l, lcConfig) -> {
                         // send the appropriate bundle for the user to update
-                        if (lcConfig.getAssignedTrackPiid() == piid) {
+                        if (lcConfig.getSessionId() == sessionId) {
                             final Map<LoudnessCodecInfo, Set<MediaCodec>> mediaCodecsMap =
                                     lcConfig.getRegisteredMediaCodecs();
                             for (LoudnessCodecInfo codecInfo : mediaCodecsMap.keySet()) {
@@ -111,7 +110,12 @@
                                                             bundle));
 
                                     if (!bundle.isDefinitelyEmpty()) {
-                                        mediaCodec.setParameters(bundle);
+                                        try {
+                                            mediaCodec.setParameters(bundle);
+                                        } catch (IllegalStateException e) {
+                                            Log.w(TAG, "Cannot set loudness bundle on media codec "
+                                                    + mediaCodec);
+                                        }
                                     }
                                     if (canBreak) {
                                         break;
@@ -145,7 +149,7 @@
         }
 
         void addLoudnessCodecListener(@NonNull CallbackUtil.DispatcherStub dispatcher,
-                @NonNull LoudnessCodecConfigurator configurator,
+                @NonNull LoudnessCodecController configurator,
                 @NonNull @CallbackExecutor Executor executor,
                 @NonNull OnLoudnessCodecUpdateListener listener) {
             Objects.requireNonNull(configurator);
@@ -160,15 +164,15 @@
             }
         }
 
-        void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
+        void removeLoudnessCodecListener(@NonNull LoudnessCodecController configurator) {
             Objects.requireNonNull(configurator);
 
             OnLoudnessCodecUpdateListener listenerToRemove = null;
             synchronized (mLock) {
-                Iterator<Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>> iterator =
+                Iterator<Entry<OnLoudnessCodecUpdateListener, LoudnessCodecController>> iterator =
                         mConfiguratorListener.entrySet().iterator();
                 while (iterator.hasNext()) {
-                    Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e =
+                    Entry<OnLoudnessCodecUpdateListener, LoudnessCodecController> e =
                             iterator.next();
                     if (e.getValue() == configurator) {
                         final OnLoudnessCodecUpdateListener listener = e.getKey();
@@ -208,7 +212,7 @@
     }
 
     /** @hide */
-    public void addLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator,
+    public void addLoudnessCodecListener(@NonNull LoudnessCodecController configurator,
                                          @NonNull @CallbackExecutor Executor executor,
                                          @NonNull OnLoudnessCodecUpdateListener listener) {
         LoudnessCodecUpdatesDispatcherStub.getInstance().addLoudnessCodecListener(this,
@@ -216,52 +220,52 @@
     }
 
     /** @hide */
-    public void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
+    public void removeLoudnessCodecListener(@NonNull LoudnessCodecController configurator) {
         LoudnessCodecUpdatesDispatcherStub.getInstance().removeLoudnessCodecListener(configurator);
     }
 
     /** @hide */
-    public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+    public void startLoudnessCodecUpdates(int sessionId) {
         try {
-            mAudioService.startLoudnessCodecUpdates(piid, codecInfoList);
+            mAudioService.startLoudnessCodecUpdates(sessionId);
         }  catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
     }
 
     /** @hide */
-    public void stopLoudnessCodecUpdates(int piid) {
+    public void stopLoudnessCodecUpdates(int sessionId) {
         try {
-            mAudioService.stopLoudnessCodecUpdates(piid);
+            mAudioService.stopLoudnessCodecUpdates(sessionId);
         }  catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
     }
 
     /** @hide */
-    public void addLoudnessCodecInfo(int piid, int mediaCodecHash,
+    public void addLoudnessCodecInfo(int sessionId, int mediaCodecHash,
             @NonNull LoudnessCodecInfo mcInfo) {
         try {
-            mAudioService.addLoudnessCodecInfo(piid, mediaCodecHash, mcInfo);
+            mAudioService.addLoudnessCodecInfo(sessionId, mediaCodecHash, mcInfo);
         }  catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
     }
 
     /** @hide */
-    public void removeLoudnessCodecInfo(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+    public void removeLoudnessCodecInfo(int sessionId, @NonNull LoudnessCodecInfo mcInfo) {
         try {
-            mAudioService.removeLoudnessCodecInfo(piid, mcInfo);
+            mAudioService.removeLoudnessCodecInfo(sessionId, mcInfo);
         }  catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
     }
 
     /** @hide */
-    public Bundle getLoudnessCodecParams(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+    public Bundle getLoudnessCodecParams(@NonNull LoudnessCodecInfo mcInfo) {
         Bundle loudnessParams = null;
         try {
-            loudnessParams = new Bundle(mAudioService.getLoudnessParams(piid, mcInfo));
+            loudnessParams = new Bundle(mAudioService.getLoudnessParams(mcInfo));
         }  catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index 587e35b..5e40eee 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import static com.android.media.codec.flags.Flags.FLAG_CODEC_IMPORTANCE;
+import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -121,6 +122,10 @@
  * <tr><td>{@link #KEY_MPEGH_REFERENCE_CHANNEL_LAYOUT}</td>
  *     <td>Integer</td><td><b>decoder-only</b>, optional, if content is MPEG-H audio,
  *         specifies the preferred reference channel layout of the stream.</td></tr>
+ * <tr><td>{@link #KEY_MAX_BUFFER_BATCH_OUTPUT_SIZE}</td><td>Integer</td><td>optional, used with
+ *         large audio frame support, specifies max size of output buffer in bytes.</td></tr>
+ * <tr><td>{@link #KEY_BUFFER_BATCH_THRESHOLD_OUTPUT_SIZE}</td><td>Integer</td><td>optional,
+ *         used with large audio frame support, specifies threshold output size in bytes.</td></tr>
  * </table>
  *
  * Subtitle formats have the following keys:
@@ -459,6 +464,50 @@
     public static final String KEY_MAX_INPUT_SIZE = "max-input-size";
 
     /**
+     * A key describing the maximum output buffer size in bytes when using
+     * large buffer mode containing multiple access units.
+     *
+     * When not-set - codec functions with one access-unit per frame.
+     * When set less than the size of two access-units - will make codec
+     * operate in single access-unit per output frame.
+     * When set to a value too big - The component or the framework will
+     * override this value to a reasonable max size not exceeding typical
+     * 10 seconds of data (device dependent) when set to a value larger than
+     * that. The value final value used will be returned in the output format.
+     *
+     * The associated value is an integer
+     *
+     * @see FEATURE_MultipleFrames
+     */
+    @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+    public static final String KEY_BUFFER_BATCH_MAX_OUTPUT_SIZE = "buffer-batch-max-output-size";
+
+    /**
+     * A key describing the threshold output size in bytes when using large buffer
+     * mode containing multiple access units.
+     *
+     * This is an optional parameter.
+     *
+     * If not set - the component can set this to a reasonable value.
+     * If set larger than max size, the components will
+     * clip this setting to maximum buffer batching output size.
+     *
+     * The component will return a partial output buffer if the output buffer reaches or
+     * surpass this limit.
+     *
+     * Threshold size should be always less or equal to KEY_MAX_BUFFER_BATCH_OUTPUT_SIZE.
+     * The final setting of this value as determined by the component will be returned
+     * in the output format
+     *
+     * The associated value is an integer
+     *
+     * @see FEATURE_MultipleFrames
+     */
+    @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+    public static final String KEY_BUFFER_BATCH_THRESHOLD_OUTPUT_SIZE =
+            "buffer-batch-threshold-output-size";
+
+    /**
      * A key describing the pixel aspect ratio width.
      * The associated value is an integer
      */
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index 8e9c079..a0f8ae5 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -1154,6 +1154,7 @@
             setDataSource(afd);
             return true;
         } catch (NullPointerException | SecurityException | IOException ex) {
+            Log.w(TAG, "Error setting data source via ContentResolver", ex);
             return false;
         }
     }
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index a49782f..691aa77 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -3105,9 +3105,8 @@
                                 mStub, mDiscoveryPreference);
                     }
 
-                    if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) {
-                        unregisterRouterStubLocked();
-                    }
+                    unregisterRouterStubIfNeededLocked();
+
                 } catch (RemoteException ex) {
                     Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex);
                 }
@@ -3315,13 +3314,12 @@
                             obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, controller));
                 }
 
-                if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) {
-                    try {
-                        unregisterRouterStubLocked();
-                    } catch (RemoteException ex) {
-                        ex.rethrowFromSystemServer();
-                    }
+                try {
+                    unregisterRouterStubIfNeededLocked();
+                } catch (RemoteException ex) {
+                    ex.rethrowFromSystemServer();
                 }
+
             }
         }
 
@@ -3335,8 +3333,10 @@
         }
 
         @GuardedBy("mLock")
-        private void unregisterRouterStubLocked() throws RemoteException {
-            if (mStub != null) {
+        private void unregisterRouterStubIfNeededLocked() throws RemoteException {
+            if (mStub != null
+                    && mRouteCallbackRecords.isEmpty()
+                    && mNonSystemRoutingControllers.isEmpty()) {
                 mMediaRouterService.unregisterRouter2(mStub);
                 mStub = null;
             }
diff --git a/media/java/android/media/audiofx/Virtualizer.java b/media/java/android/media/audiofx/Virtualizer.java
index 74b6fc1..71147f4 100644
--- a/media/java/android/media/audiofx/Virtualizer.java
+++ b/media/java/android/media/audiofx/Virtualizer.java
@@ -46,6 +46,11 @@
  * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions.
  * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling
  * audio effects.
+ *
+ * @deprecated use the {@link android.media.Spatializer} class to query the capabilities of the
+ *     platform with regards to spatialization, a different name for audio channel virtualization,
+ *     and the {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)} to
+ *     characterize how you want your content to be played when spatialization is supported.
  */
 
 public class Virtualizer extends AudioEffect {
diff --git a/media/java/android/media/tv/BroadcastInfoRequest.java b/media/java/android/media/tv/BroadcastInfoRequest.java
index cbd8c1f..694756c 100644
--- a/media/java/android/media/tv/BroadcastInfoRequest.java
+++ b/media/java/android/media/tv/BroadcastInfoRequest.java
@@ -32,7 +32,8 @@
 public abstract class BroadcastInfoRequest implements Parcelable {
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE})
+    @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE,
+            REQUEST_OPTION_ONEWAY, REQUEST_OPTION_ONESHOT})
     public @interface RequestOption {}
 
     /**
@@ -47,6 +48,18 @@
      * first time, new values are detected.
      */
     public static final int REQUEST_OPTION_AUTO_UPDATE = 1;
+    /**
+     * Request option: one-way
+     * <p> With this option, no response is expected after sending the request.
+     * @hide
+     */
+    public static final int REQUEST_OPTION_ONEWAY = 2;
+    /**
+     * Request option: one-shot
+     * <p> With this option, only one response will be given per request.
+     * @hide
+     */
+    public static final int REQUEST_OPTION_ONESHOT = 3;
 
     public static final @NonNull Parcelable.Creator<BroadcastInfoRequest> CREATOR =
             new Parcelable.Creator<BroadcastInfoRequest>() {
diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl
index 0f8a00a..8978277 100644
--- a/media/java/android/media/tv/ITvInputClient.aidl
+++ b/media/java/android/media/tv/ITvInputClient.aidl
@@ -44,6 +44,7 @@
     void onTrackSelected(int type, in String trackId, int seq);
     void onVideoAvailable(int seq);
     void onVideoUnavailable(int reason, int seq);
+    void onVideoFreezeUpdated(boolean isFrozen, int seq);
     void onContentAllowed(int seq);
     void onContentBlocked(in String rating, int seq);
     void onLayoutSurface(int left, int top, int right, int bottom, int seq);
diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl
index a52e9a5..8e2702a 100644
--- a/media/java/android/media/tv/ITvInputSessionCallback.aidl
+++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl
@@ -41,6 +41,7 @@
     void onTrackSelected(int type, in String trackId);
     void onVideoAvailable();
     void onVideoUnavailable(int reason);
+    void onVideoFreezeUpdated(boolean isFrozen);
     void onContentAllowed();
     void onContentBlocked(in String rating);
     void onLayoutSurface(int left, int top, int right, int bottom);
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index 51b2542..caddd8a 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -740,6 +740,15 @@
         }
 
         /**
+         * This is called when the video freeze state has been updated.
+         * If {@code true}, the video is frozen on the last frame while audio playback continues.
+         * @param session A {@link TvInputManager.Session} associated with this callback.
+         * @param isFrozen Whether the video is frozen
+         */
+        public void onVideoFreezeUpdated(Session session, boolean isFrozen) {
+        }
+
+        /**
          * This is called when the current program content turns out to be allowed to watch since
          * its content rating is not blocked by parental controls.
          *
@@ -1030,6 +1039,19 @@
             });
         }
 
+        void postVideoFreezeUpdated(boolean isFrozen) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onVideoFreezeUpdated(mSession, isFrozen);
+                    if (mSession.mIAppNotificationEnabled
+                            && mSession.getInteractiveAppSession() != null) {
+                        mSession.getInteractiveAppSession().notifyVideoFreezeUpdated(isFrozen);
+                    }
+                }
+            });
+        }
+
         void postContentAllowed() {
             mHandler.post(new Runnable() {
                 @Override
@@ -1546,6 +1568,18 @@
             }
 
             @Override
+            public void onVideoFreezeUpdated(boolean isFrozen, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postVideoFreezeUpdated(isFrozen);
+                }
+            }
+
+            @Override
             public void onContentAllowed(int seq) {
                 synchronized (mSessionCallbackRecordMap) {
                     SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 76d8e50..6301a27 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -763,6 +763,34 @@
         }
 
         /**
+         * Informs the application that the video freeze state has been updated.
+         *
+         * When {@code true}, the video is frozen on the last frame but audio playback remains
+         * active.
+         *
+         * @param isFrozen Whether or not the video is frozen
+         * @hide
+         */
+        public void notifyVideoFreezeUpdated(boolean isFrozen) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "notifyVideoFreezeUpdated");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onVideoFreezeUpdated(isFrozen);
+                        }
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "error in notifyVideoFreezeUpdated", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Sends an updated list of all audio presentations available from a Next Generation Audio
          * service. This is used by the framework to maintain the audio presentation information for
          * a given track of {@link TvTrackInfo#TYPE_AUDIO}, which in turn is used by
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 4316d05..0f58b29 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -88,6 +88,7 @@
     void notifyTracksChanged(in IBinder sessionToken, in List<TvTrackInfo> tracks, int userId);
     void notifyVideoAvailable(in IBinder sessionToken, int userId);
     void notifyVideoUnavailable(in IBinder sessionToken, int reason, int userId);
+    void notifyVideoFreezeUpdated(in IBinder sessionToken, boolean isFrozen, int userId);
     void notifyContentAllowed(in IBinder sessionToken, int userId);
     void notifyContentBlocked(in IBinder sessionToken, in String rating, int userId);
     void notifySignalStrength(in IBinder sessionToken, int stength, int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index ba7cf13..06808c9 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -67,6 +67,7 @@
     void notifyTracksChanged(in List<TvTrackInfo> tracks);
     void notifyVideoAvailable();
     void notifyVideoUnavailable(int reason);
+    void notifyVideoFreezeUpdated(boolean isFrozen);
     void notifyContentAllowed();
     void notifyContentBlocked(in String rating);
     void notifySignalStrength(int strength);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index 518b08a..77730aa 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -103,6 +103,7 @@
     private static final int DO_SEND_TIME_SHIFT_MODE = 46;
     private static final int DO_SEND_AVAILABLE_SPEEDS = 47;
     private static final int DO_SEND_SELECTED_TRACK_INFO = 48;
+    private static final int DO_NOTIFY_VIDEO_FREEZE_UPDATED = 49;
 
     private final HandlerCaller mCaller;
     private Session mSessionImpl;
@@ -364,6 +365,10 @@
                 args.recycle();
                 break;
             }
+            case DO_NOTIFY_VIDEO_FREEZE_UPDATED: {
+                mSessionImpl.notifyVideoFreezeUpdated((Boolean) msg.obj);
+                break;
+            }
             default: {
                 Log.w(TAG, "Unhandled message code: " + msg.what);
                 break;
@@ -552,6 +557,12 @@
     }
 
     @Override
+    public void notifyVideoFreezeUpdated(boolean isFrozen) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_VIDEO_FREEZE_UPDATED,
+                isFrozen));
+    }
+
+    @Override
     public void notifyContentAllowed() {
         mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_NOTIFY_CONTENT_ALLOWED));
     }
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index bf4379f..8a340f6 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -1731,6 +1731,22 @@
         }
 
         /**
+         * Notifies Interactive app session when the video freeze state is updated
+         * @param isFrozen Whether or not the video is frozen
+         */
+        public void notifyVideoFreezeUpdated(boolean isFrozen) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyVideoFreezeUpdated(mToken, isFrozen, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Notifies Interactive APP session when content is allowed.
          */
         public void notifyContentAllowed() {
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 7936403..5247a0e 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -882,6 +882,15 @@
         }
 
         /**
+         * Called when video becomes frozen or unfrozen. Audio playback will continue while
+         * video will be frozen to the last frame if {@code true}.
+         * @param isFrozen Whether or not the video is frozen.
+         * @hide
+         */
+        public void onVideoFreezeUpdated(boolean isFrozen) {
+        }
+
+        /**
          * Called when content is allowed.
          */
         public void onContentAllowed() {
@@ -1770,6 +1779,13 @@
             onVideoUnavailable(reason);
         }
 
+        void notifyVideoFreezeUpdated(boolean isFrozen) {
+            if (DEBUG) {
+                Log.d(TAG, "notifyVideoFreezeUpdated (isFrozen=" + isFrozen + ")");
+            }
+            onVideoFreezeUpdated(isFrozen);
+        }
+
         void notifyContentAllowed() {
             if (DEBUG) {
                 Log.d(TAG, "notifyContentAllowed");
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 40a12e4..5bb61c2 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -719,6 +719,22 @@
     }
 
     /**
+     * Alerts the TV Interactive app that the video freeze state has been updated.
+     * If {@code true}, the video is frozen on the last frame while audio playback continues.
+     *
+     * @param isFrozen Whether the video is frozen.
+     * @hide
+     */
+    public void notifyVideoFreezeUpdated(boolean isFrozen) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyVideoFreezeUpdated");
+        }
+        if (mSession != null) {
+            mSession.notifyVideoFreezeUpdated(isFrozen);
+        }
+    }
+
+    /**
      * Sends signing result to related TV interactive app.
      *
      * <p>This is used when the corresponding server of the broadcast-independent interactive
diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java
deleted file mode 100644
index 74e5612..0000000
--- a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- * Copyright (C) 2023 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.loudnesscodecapitest;
-
-import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyList;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.res.AssetFileDescriptor;
-import android.media.AudioAttributes;
-import android.media.AudioFormat;
-import android.media.AudioTrack;
-import android.media.IAudioService;
-import android.media.LoudnessCodecConfigurator;
-import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
-import android.media.MediaCodec;
-import android.media.MediaExtractor;
-import android.media.MediaFormat;
-import android.os.PersistableBundle;
-import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.util.Log;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.util.List;
-import java.util.concurrent.Executors;
-
-/**
- * Unit tests for {@link LoudnessCodecConfigurator} checking the internal interactions with a mocked
- * {@link IAudioService} without any real IPC interactions.
- */
-@Presubmit
-@RunWith(AndroidJUnit4.class)
-public class LoudnessCodecConfiguratorTest {
-    private static final String TAG = "LoudnessCodecConfiguratorTest";
-
-    private static final String TEST_MEDIA_AUDIO_CODEC_PREFIX = "audio/";
-    private static final int TEST_AUDIO_TRACK_BUFFER_SIZE = 2048;
-    private static final int TEST_AUDIO_TRACK_SAMPLERATE = 48000;
-    private static final int TEST_AUDIO_TRACK_CHANNELS = 2;
-
-    @Rule
-    public final MockitoRule mockito = MockitoJUnit.rule();
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
-    @Mock
-    private IAudioService mAudioService;
-
-    private LoudnessCodecConfigurator mLcc;
-
-    @Before
-    public void setUp() {
-        mLcc = LoudnessCodecConfigurator.createForTesting(mAudioService,
-                Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void setAudioTrack_callsAudioServiceStart() throws Exception {
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec);
-            mLcc.setAudioTrack(track);
-
-            verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
-                    anyList());
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void getLoudnessCodecParams_callsAudioServiceGetLoudness() throws Exception {
-        when(mAudioService.getLoudnessParams(anyInt(), any())).thenReturn(new PersistableBundle());
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.getLoudnessCodecParams(track, mediaCodec);
-
-            verify(mAudioService).getLoudnessParams(eq(track.getPlayerIId()), any());
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void setAudioTrack_addsAudioServicePiidCodecs() throws Exception {
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec);
-            mLcc.setAudioTrack(track);
-
-            verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void setAudioTrackTwice_ignoresSecondCall() throws Exception {
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec);
-            mLcc.setAudioTrack(track);
-            mLcc.setAudioTrack(track);
-
-            verify(mAudioService, times(1)).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
-                    anyList());
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void setTrackNull_stopCodecUpdates() throws Exception {
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec);
-            mLcc.setAudioTrack(track);
-
-            mLcc.setAudioTrack(null);  // stops updates
-            verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId()));
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void addMediaCodecTwice_triggersIAE() throws Exception {
-        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec);
-
-            assertThrows(IllegalArgumentException.class, () -> mLcc.addMediaCodec(mediaCodec));
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void addUnconfiguredMediaCodec_returnsFalse() throws Exception {
-        final MediaCodec mediaCodec = MediaCodec.createDecoderByType("audio/mpeg");
-
-        try {
-            assertFalse(mLcc.addMediaCodec(mediaCodec));
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void setClearTrack_removeAllAudioServicePiidCodecs() throws Exception {
-        final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec1 = createAndConfigureMediaCodec();
-        final MediaCodec mediaCodec2 = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec1);
-            mLcc.setAudioTrack(track);
-            verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
-                    argument.capture());
-            assertEquals(argument.getValue().size(), 1);
-
-            mLcc.addMediaCodec(mediaCodec2);
-            mLcc.setAudioTrack(null);
-            verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId()));
-        } finally {
-            mediaCodec1.release();
-            mediaCodec2.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void removeAddedMediaCodecAfterSetTrack_callsAudioServiceRemoveCodec() throws Exception {
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec);
-            mLcc.setAudioTrack(track);
-            mLcc.removeMediaCodec(mediaCodec);
-
-            verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void addMediaCodecAfterSetTrack_callsAudioServiceAdd() throws Exception {
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec1 = createAndConfigureMediaCodec();
-        final MediaCodec mediaCodec2 = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec1);
-            mLcc.setAudioTrack(track);
-            verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
-
-            mLcc.addMediaCodec(mediaCodec2);
-            verify(mAudioService).addLoudnessCodecInfo(eq(track.getPlayerIId()), anyInt(), any());
-        } finally {
-            mediaCodec1.release();
-            mediaCodec2.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void removeMediaCodecAfterSetTrack_callsAudioServiceRemove() throws Exception {
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec);
-            mLcc.setAudioTrack(track);
-            verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
-
-            mLcc.removeMediaCodec(mediaCodec);
-            verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
-        } finally {
-            mediaCodec.release();
-        }
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void removeWrongMediaCodecAfterSetTrack_triggersIAE() throws Exception {
-        final AudioTrack track = createAudioTrack();
-        final MediaCodec mediaCodec1 = createAndConfigureMediaCodec();
-        final MediaCodec mediaCodec2 = createAndConfigureMediaCodec();
-
-        try {
-            mLcc.addMediaCodec(mediaCodec1);
-            mLcc.setAudioTrack(track);
-            verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
-
-            assertThrows(IllegalArgumentException.class,
-                    () -> mLcc.removeMediaCodec(mediaCodec2));
-        } finally {
-            mediaCodec1.release();
-            mediaCodec2.release();
-        }
-    }
-
-    private static AudioTrack createAudioTrack() {
-        return new AudioTrack.Builder()
-                .setAudioAttributes(new AudioAttributes.Builder().build())
-                .setBufferSizeInBytes(TEST_AUDIO_TRACK_BUFFER_SIZE)
-                .setAudioFormat(new AudioFormat.Builder()
-                        .setChannelMask(TEST_AUDIO_TRACK_CHANNELS)
-                        .setSampleRate(TEST_AUDIO_TRACK_SAMPLERATE).build())
-                .build();
-    }
-
-    private MediaCodec createAndConfigureMediaCodec() throws Exception {
-        AssetFileDescriptor testFd = InstrumentationRegistry.getInstrumentation().getContext()
-                .getResources()
-                .openRawResourceFd(R.raw.noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4);
-
-        MediaExtractor extractor;
-        extractor = new MediaExtractor();
-        try {
-            extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
-                testFd.getLength());
-            assertEquals("wrong number of tracks", 1, extractor.getTrackCount());
-            MediaFormat format = extractor.getTrackFormat(0);
-            String mime = format.getString(MediaFormat.KEY_MIME);
-            assertTrue("not an audio file", mime.startsWith(TEST_MEDIA_AUDIO_CODEC_PREFIX));
-            final MediaCodec mediaCodec = MediaCodec.createDecoderByType(mime);
-
-            Log.v(TAG, "configuring with " + format);
-            mediaCodec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
-            return mediaCodec;
-        } finally {
-            testFd.close();
-            extractor.release();
-        }
-    }
-}
diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java
new file mode 100644
index 0000000..4f6ede5
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2023 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.loudnesscodecapitest;
+
+import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.media.LoudnessCodecController;
+import android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.Executors;
+
+/**
+ * Unit tests for {@link LoudnessCodecController} checking the internal interactions with a mocked
+ * {@link IAudioService} without any real IPC interactions.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LoudnessCodecControllerTest {
+    private static final String TAG = "LoudnessCodecConfiguratorTest";
+
+    private static final String TEST_MEDIA_AUDIO_CODEC_PREFIX = "audio/";
+    private static final int TEST_AUDIO_TRACK_BUFFER_SIZE = 2048;
+    private static final int TEST_AUDIO_TRACK_SAMPLERATE = 48000;
+    private static final int TEST_AUDIO_TRACK_CHANNELS = 2;
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Mock
+    private IAudioService mAudioService;
+
+    private LoudnessCodecController mLcc;
+
+    private int mSessionId;
+
+    @Before
+    public void setUp() {
+        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final AudioManager audioManager = (AudioManager) context.getSystemService(
+                AudioManager.class);
+        mSessionId = 0;
+        if (audioManager != null) {
+            mSessionId = audioManager.generateAudioSessionId();
+        }
+        mLcc = LoudnessCodecController.createForTesting(mSessionId,
+                Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {
+                }, mAudioService);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void createLcc_callsAudioServiceStart() throws Exception {
+        verify(mAudioService).startLoudnessCodecUpdates(eq(mSessionId));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void getLoudnessCodecParams_callsAudioServiceGetLoudness() throws Exception {
+        when(mAudioService.getLoudnessParams(any())).thenReturn(new PersistableBundle());
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        try {
+            mLcc.addMediaCodec(mediaCodec);
+            mLcc.getLoudnessCodecParams(mediaCodec);
+
+            verify(mAudioService).getLoudnessParams(any());
+        } finally {
+            mediaCodec.release();
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void release_stopCodecUpdates() throws Exception {
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        try {
+            mLcc.addMediaCodec(mediaCodec);
+            mLcc.release();  // stops updats
+
+            verify(mAudioService).stopLoudnessCodecUpdates(eq(mSessionId));
+        } finally {
+            mediaCodec.release();
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void addMediaCodecTwice_triggersIAE() throws Exception {
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        try {
+            mLcc.addMediaCodec(mediaCodec);
+
+            assertThrows(IllegalArgumentException.class, () -> mLcc.addMediaCodec(mediaCodec));
+        } finally {
+            mediaCodec.release();
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void addUnconfiguredMediaCodec_returnsFalse() throws Exception {
+        final MediaCodec mediaCodec = MediaCodec.createDecoderByType("audio/mpeg");
+
+        try {
+            assertFalse(mLcc.addMediaCodec(mediaCodec));
+        } finally {
+            mediaCodec.release();
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void removeAddedMediaCodecAfterSetTrack_callsAudioServiceRemoveCodec() throws Exception {
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        try {
+            mLcc.addMediaCodec(mediaCodec);
+            mLcc.removeMediaCodec(mediaCodec);
+
+            verify(mAudioService).removeLoudnessCodecInfo(eq(mSessionId), any());
+        } finally {
+            mediaCodec.release();
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void addMediaCodec_callsAudioServiceAdd() throws Exception {
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        try {
+            mLcc.addMediaCodec(mediaCodec);
+            verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any());
+        } finally {
+            mediaCodec.release();
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void removeMediaCodec_callsAudioServiceRemove() throws Exception {
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        try {
+            mLcc.addMediaCodec(mediaCodec);
+            verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any());
+
+            mLcc.removeMediaCodec(mediaCodec);
+            verify(mAudioService).removeLoudnessCodecInfo(eq(mSessionId), any());
+        } finally {
+            mediaCodec.release();
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void removeWrongMediaCodec_triggersIAE() throws Exception {
+        final MediaCodec mediaCodec1 = createAndConfigureMediaCodec();
+        final MediaCodec mediaCodec2 = createAndConfigureMediaCodec();
+
+        try {
+            mLcc.addMediaCodec(mediaCodec1);
+            verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any());
+
+            assertThrows(IllegalArgumentException.class,
+                    () -> mLcc.removeMediaCodec(mediaCodec2));
+        } finally {
+            mediaCodec1.release();
+            mediaCodec2.release();
+        }
+    }
+
+    private MediaCodec createAndConfigureMediaCodec() throws Exception {
+        AssetFileDescriptor testFd = InstrumentationRegistry.getInstrumentation().getContext()
+                .getResources()
+                .openRawResourceFd(R.raw.noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4);
+
+        MediaExtractor extractor;
+        extractor = new MediaExtractor();
+        try {
+            extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
+                    testFd.getLength());
+            assertEquals("wrong number of tracks", 1, extractor.getTrackCount());
+            MediaFormat format = extractor.getTrackFormat(0);
+            String mime = format.getString(MediaFormat.KEY_MIME);
+            assertTrue("not an audio file", mime.startsWith(TEST_MEDIA_AUDIO_CODEC_PREFIX));
+            final MediaCodec mediaCodec = MediaCodec.createDecoderByType(mime);
+
+            Log.v(TAG, "configuring with " + format);
+            mediaCodec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
+            return mediaCodec;
+        } finally {
+            testFd.close();
+            extractor.release();
+        }
+    }
+}
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index 7573474..1046d8e9 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -72,7 +72,7 @@
     method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle);
     method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context);
     method @Nullable public android.nfc.NfcAntennaInfo getNfcAntennaInfo();
-    method @FlaggedApi("android.nfc.enable_nfc_charging") @Nullable public android.nfc.WlcLDeviceInfo getWlcLDeviceInfo();
+    method @FlaggedApi("android.nfc.enable_nfc_charging") @Nullable public android.nfc.WlcListenerDeviceInfo getWlcListenerDeviceInfo();
     method public boolean ignore(android.nfc.Tag, int, android.nfc.NfcAdapter.OnTagRemovedListener, android.os.Handler);
     method public boolean isEnabled();
     method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean isObserveModeSupported();
@@ -175,18 +175,18 @@
     ctor public TagLostException(String);
   }
 
-  @FlaggedApi("android.nfc.enable_nfc_charging") public final class WlcLDeviceInfo implements android.os.Parcelable {
-    ctor public WlcLDeviceInfo(double, double, double, int);
+  @FlaggedApi("android.nfc.enable_nfc_charging") public final class WlcListenerDeviceInfo implements android.os.Parcelable {
+    ctor public WlcListenerDeviceInfo(int, double, double, int);
     method public int describeContents();
-    method public double getBatteryLevel();
-    method public double getProductId();
+    method @FloatRange(from=0.0, to=100.0) public double getBatteryLevel();
+    method public int getProductId();
     method public int getState();
     method public double getTemperature();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field public static final int CONNECTED_CHARGING = 2; // 0x2
-    field public static final int CONNECTED_DISCHARGING = 3; // 0x3
-    field @NonNull public static final android.os.Parcelable.Creator<android.nfc.WlcLDeviceInfo> CREATOR;
-    field public static final int DISCONNECTED = 1; // 0x1
+    field @NonNull public static final android.os.Parcelable.Creator<android.nfc.WlcListenerDeviceInfo> CREATOR;
+    field public static final int STATE_CONNECTED_CHARGING = 2; // 0x2
+    field public static final int STATE_CONNECTED_DISCHARGING = 3; // 0x3
+    field public static final int STATE_DISCONNECTED = 1; // 0x1
   }
 
 }
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 40672a1..3524f8c 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -8,7 +8,6 @@
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable();
     method @FlaggedApi("android.nfc.enable_nfc_reader_option") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableReaderOption(boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableSecureNfc(boolean);
-    method @FlaggedApi("android.nfc.enable_nfc_charging") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableWlc(boolean);
     method @FlaggedApi("android.nfc.enable_nfc_mainline") public int getAdapterState();
     method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public java.util.Map<java.lang.String,java.lang.Boolean> getTagIntentAppPreferenceForUser(int);
     method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOn();
@@ -20,6 +19,7 @@
     method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean setControllerAlwaysOn(boolean);
     method @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setReaderMode(boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setTagIntentAppPreferenceForUser(int, @NonNull String, boolean);
+    method @FlaggedApi("android.nfc.enable_nfc_charging") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean setWlcEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void unregisterControllerAlwaysOnListener(@NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener);
     method @FlaggedApi("android.nfc.enable_nfc_charging") public void unregisterWlcStateListener(@NonNull android.nfc.NfcAdapter.WlcStateListener);
     field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC";
@@ -37,7 +37,7 @@
   }
 
   @FlaggedApi("android.nfc.enable_nfc_charging") public static interface NfcAdapter.WlcStateListener {
-    method public void onWlcStateChanged(@NonNull android.nfc.WlcLDeviceInfo);
+    method public void onWlcStateChanged(@NonNull android.nfc.WlcListenerDeviceInfo);
   }
 
 }
@@ -45,7 +45,7 @@
 package android.nfc.cardemulation {
 
   public final class CardEmulation {
-    method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public android.nfc.cardemulation.ApduServiceInfo getPreferredPaymentService();
+    method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static android.content.ComponentName getPreferredPaymentService(@NonNull android.content.Context);
     method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int);
   }
 
diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl
index bec62c5..63c3414 100644
--- a/nfc/java/android/nfc/INfcAdapter.aidl
+++ b/nfc/java/android/nfc/INfcAdapter.aidl
@@ -32,8 +32,8 @@
 import android.nfc.INfcDta;
 import android.nfc.INfcWlcStateListener;
 import android.nfc.NfcAntennaInfo;
+import android.nfc.WlcListenerDeviceInfo;
 import android.os.Bundle;
-import android.nfc.WlcLDeviceInfo;
 
 /**
  * @hide
@@ -90,11 +90,11 @@
     boolean setObserveMode(boolean enabled);
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)")
-    boolean enableWlc(boolean enable);
+    boolean setWlcEnabled(boolean enable);
     boolean isWlcEnabled();
     void registerWlcStateListener(in INfcWlcStateListener listener);
     void unregisterWlcStateListener(in INfcWlcStateListener listener);
-    WlcLDeviceInfo getWlcLDeviceInfo();
+    WlcListenerDeviceInfo getWlcListenerDeviceInfo();
 
     void updateDiscoveryTechnology(IBinder b, int pollFlags, int listenFlags);
 
diff --git a/nfc/java/android/nfc/INfcWlcStateListener.aidl b/nfc/java/android/nfc/INfcWlcStateListener.aidl
index c2b7075..584eb9a 100644
--- a/nfc/java/android/nfc/INfcWlcStateListener.aidl
+++ b/nfc/java/android/nfc/INfcWlcStateListener.aidl
@@ -16,7 +16,7 @@
 
 package android.nfc;
 
-import android.nfc.WlcLDeviceInfo;
+import android.nfc.WlcListenerDeviceInfo;
 /**
  * @hide
  */
@@ -24,7 +24,7 @@
   /**
    * Called whenever NFC WLC state changes
    *
-   * @param wlcLDeviceInfo NFC wlc listener information
+   * @param wlcListenerDeviceInfo NFC wlc listener information
    */
-  void onWlcStateChanged(in WlcLDeviceInfo wlcLDeviceInfo);
+  void onWlcStateChanged(in WlcListenerDeviceInfo wlcListenerDeviceInfo);
 }
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 68c16e6..11eb97b 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -2820,13 +2820,12 @@
     @SystemApi
     @FlaggedApi(Flags.FLAG_ENABLE_NFC_CHARGING)
     @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
-    public boolean enableWlc(boolean enable) {
+    public boolean setWlcEnabled(boolean enable) {
         if (!sHasNfcWlcFeature) {
             throw new UnsupportedOperationException();
         }
         try {
-            return sService.enableWlc(enable);
-
+            return sService.setWlcEnabled(enable);
         } catch (RemoteException e) {
             attemptDeadServiceRecovery(e);
             // Try one more time
@@ -2835,7 +2834,7 @@
                 return false;
             }
             try {
-                return sService.enableWlc(enable);
+                return sService.setWlcEnabled(enable);
             } catch (RemoteException ee) {
                 Log.e(TAG, "Failed to recover NFC Service.");
             }
@@ -2887,7 +2886,7 @@
         /**
          * Called on NFC WLC state changes
          */
-        void onWlcStateChanged(@NonNull WlcLDeviceInfo wlcLDeviceInfo);
+        void onWlcStateChanged(@NonNull WlcListenerDeviceInfo wlcListenerDeviceInfo);
     }
 
     /**
@@ -2945,12 +2944,12 @@
      */
     @FlaggedApi(Flags.FLAG_ENABLE_NFC_CHARGING)
     @Nullable
-    public WlcLDeviceInfo getWlcLDeviceInfo() {
+    public WlcListenerDeviceInfo getWlcListenerDeviceInfo() {
         if (!sHasNfcWlcFeature) {
             throw new UnsupportedOperationException();
         }
         try {
-            return sService.getWlcLDeviceInfo();
+            return sService.getWlcListenerDeviceInfo();
         } catch (RemoteException e) {
             attemptDeadServiceRecovery(e);
             // Try one more time
@@ -2959,7 +2958,7 @@
                 return null;
             }
             try {
-                return sService.getWlcLDeviceInfo();
+                return sService.getWlcListenerDeviceInfo();
             } catch (RemoteException ee) {
                 Log.e(TAG, "Failed to recover NFC Service.");
             }
diff --git a/nfc/java/android/nfc/NfcWlcStateListener.java b/nfc/java/android/nfc/NfcWlcStateListener.java
index 8d79310..890cb09 100644
--- a/nfc/java/android/nfc/NfcWlcStateListener.java
+++ b/nfc/java/android/nfc/NfcWlcStateListener.java
@@ -36,7 +36,7 @@
 
     private final Map<WlcStateListener, Executor> mListenerMap = new HashMap<>();
 
-    private WlcLDeviceInfo mCurrentState = null;
+    private WlcListenerDeviceInfo mCurrentState = null;
     private boolean mIsRegistered = false;
 
     public NfcWlcStateListener(@NonNull INfcAdapter adapter) {
@@ -98,8 +98,10 @@
             Executor executor = mListenerMap.get(listener);
             final long identity = Binder.clearCallingIdentity();
             try {
-                executor.execute(() -> listener.onWlcStateChanged(
-                        mCurrentState));
+                if (Flags.enableNfcCharging()) {
+                    executor.execute(() -> listener.onWlcStateChanged(
+                            mCurrentState));
+                }
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
@@ -107,9 +109,9 @@
     }
 
     @Override
-    public void onWlcStateChanged(@NonNull WlcLDeviceInfo wlcLDeviceInfo) {
+    public void onWlcStateChanged(@NonNull WlcListenerDeviceInfo wlcListenerDeviceInfo) {
         synchronized (this) {
-            mCurrentState = wlcLDeviceInfo;
+            mCurrentState = wlcListenerDeviceInfo;
 
             for (WlcStateListener cb : mListenerMap.keySet()) {
                 sendCurrentState(cb);
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.java b/nfc/java/android/nfc/WlcLDeviceInfo.java
deleted file mode 100644
index 016431e..0000000
--- a/nfc/java/android/nfc/WlcLDeviceInfo.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.nfc;
-
-import android.annotation.FlaggedApi;
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * Contains information of the nfc wireless charging listener device information.
- */
-@FlaggedApi(Flags.FLAG_ENABLE_NFC_CHARGING)
-public final class WlcLDeviceInfo implements Parcelable {
-    public static final int DISCONNECTED = 1;
-
-    public static final int CONNECTED_CHARGING = 2;
-
-    public static final int CONNECTED_DISCHARGING = 3;
-
-    private double mProductId;
-    private double mTemperature;
-    private double mBatteryLevel;
-    private int mState;
-
-    public WlcLDeviceInfo(double productId, double temperature, double batteryLevel, int state) {
-        this.mProductId = productId;
-        this.mTemperature = temperature;
-        this.mBatteryLevel = batteryLevel;
-        this.mState = state;
-    }
-
-    /**
-     * ProductId of the WLC listener device.
-     */
-    public double getProductId() {
-        return mProductId;
-    }
-
-    /**
-     * Temperature of the WLC listener device.
-     */
-    public double getTemperature() {
-        return mTemperature;
-    }
-
-    /**
-     * BatteryLevel of the WLC listener device.
-     */
-    public double getBatteryLevel() {
-        return mBatteryLevel;
-    }
-
-    /**
-     * State of the WLC listener device.
-     */
-    public int getState() {
-        return mState;
-    }
-
-    private WlcLDeviceInfo(Parcel in) {
-        this.mProductId = in.readDouble();
-        this.mTemperature = in.readDouble();
-        this.mBatteryLevel = in.readDouble();
-        this.mState = in.readInt();
-    }
-
-    public static final @NonNull Parcelable.Creator<WlcLDeviceInfo> CREATOR =
-            new Parcelable.Creator<WlcLDeviceInfo>() {
-                @Override
-                public WlcLDeviceInfo createFromParcel(Parcel in) {
-                    return new WlcLDeviceInfo(in);
-                }
-
-                @Override
-                public WlcLDeviceInfo[] newArray(int size) {
-                    return new WlcLDeviceInfo[size];
-                }
-            };
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeDouble(mProductId);
-        dest.writeDouble(mTemperature);
-        dest.writeDouble(mBatteryLevel);
-        dest.writeDouble(mState);
-    }
-}
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/nfc/java/android/nfc/WlcListenerDeviceInfo.aidl
similarity index 94%
rename from nfc/java/android/nfc/WlcLDeviceInfo.aidl
rename to nfc/java/android/nfc/WlcListenerDeviceInfo.aidl
index 33143fe..7f2ca54 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/nfc/java/android/nfc/WlcListenerDeviceInfo.aidl
@@ -16,4 +16,4 @@
 
 package android.nfc;
 
-parcelable WlcLDeviceInfo;
+parcelable WlcListenerDeviceInfo;
diff --git a/nfc/java/android/nfc/WlcListenerDeviceInfo.java b/nfc/java/android/nfc/WlcListenerDeviceInfo.java
new file mode 100644
index 0000000..45315f8
--- /dev/null
+++ b/nfc/java/android/nfc/WlcListenerDeviceInfo.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nfc;
+
+import android.annotation.FlaggedApi;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Contains information of the nfc wireless charging listener device information.
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_NFC_CHARGING)
+public final class WlcListenerDeviceInfo implements Parcelable {
+    /**
+     * Device is currently not connected with any WlcListenerDevice.
+     */
+    public static final int STATE_DISCONNECTED = 1;
+
+    /**
+     * Device is currently connected with a WlcListenerDevice and is charging it.
+     */
+    public static final int STATE_CONNECTED_CHARGING = 2;
+
+    /**
+     * Device is currently connected with a WlcListenerDevice without charging it.
+     */
+    public static final int STATE_CONNECTED_DISCHARGING = 3;
+
+    /**
+     * Possible states from {@link #getState}.
+     * @hide
+     */
+    @IntDef(prefix = { "STATE_" }, value = {
+            STATE_DISCONNECTED,
+            STATE_CONNECTED_CHARGING,
+            STATE_CONNECTED_DISCHARGING
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface WlcListenerState{}
+
+    private int mProductId;
+    private double mTemperature;
+    private double mBatteryLevel;
+    private int mState;
+
+     /**
+     * Create a new object containing wlc listener information.
+     *
+     * @param productId code for the device vendor
+     * @param temperature current temperature
+     * @param batteryLevel current battery level
+     * @param state current state
+     */
+    public WlcListenerDeviceInfo(int productId, double temperature, double batteryLevel,
+            @WlcListenerState int state) {
+        this.mProductId = productId;
+        this.mTemperature = temperature;
+        this.mBatteryLevel = batteryLevel;
+        this.mState = state;
+    }
+
+    /**
+     * ProductId of the WLC listener device.
+     * @return integer that is converted from USI Stylus VendorID[11:0].
+     */
+    public int getProductId() {
+        return mProductId;
+    }
+
+    /**
+     * Temperature of the WLC listener device.
+     * @return the value represents the temperature in °C.
+     */
+    public double getTemperature() {
+        return mTemperature;
+    }
+
+    /**
+     * BatteryLevel of the WLC listener device.
+     * @return battery level in percentage [0-100]
+     */
+    public @FloatRange(from = 0.0, to = 100.0) double getBatteryLevel() {
+        return mBatteryLevel;
+    }
+
+    /**
+     * State of the WLC listener device.
+     */
+    public @WlcListenerState int getState() {
+        return mState;
+    }
+
+    private WlcListenerDeviceInfo(Parcel in) {
+        this.mProductId = in.readInt();
+        this.mTemperature = in.readDouble();
+        this.mBatteryLevel = in.readDouble();
+        this.mState = in.readInt();
+    }
+
+    public static final @NonNull Parcelable.Creator<WlcListenerDeviceInfo> CREATOR =
+            new Parcelable.Creator<WlcListenerDeviceInfo>() {
+                @Override
+                public WlcListenerDeviceInfo createFromParcel(Parcel in) {
+                    return new WlcListenerDeviceInfo(in);
+                }
+
+                @Override
+                public WlcListenerDeviceInfo[] newArray(int size) {
+                    return new WlcListenerDeviceInfo[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mProductId);
+        dest.writeDouble(mTemperature);
+        dest.writeDouble(mBatteryLevel);
+        dest.writeInt(mState);
+    }
+}
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index 0943392..9d38e4c 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -16,6 +16,7 @@
 
 package android.nfc.cardemulation;
 
+import android.Manifest;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -23,6 +24,7 @@
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.SystemApi;
+import android.annotation.UserHandleAware;
 import android.annotation.UserIdInt;
 import android.app.Activity;
 import android.content.ComponentName;
@@ -1138,31 +1140,28 @@
     }
 
     /**
-     * Returns the {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT} for the given user.
+     * Returns the value of {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT}.
+     *
+     * @param context A context
+     * @return A ComponentName for the setting value, or null.
      *
      * @hide
      */
     @SystemApi
+    @UserHandleAware
     @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
+    @SuppressWarnings("AndroidFrameworkClientSidePermissionCheck")
     @FlaggedApi(android.permission.flags.Flags.FLAG_WALLET_ROLE_ENABLED)
     @Nullable
-    public ApduServiceInfo getPreferredPaymentService() {
-        try {
-            return sService.getPreferredPaymentService(mContext.getUser().getIdentifier());
-        } catch (RemoteException e) {
-            // Try one more time
-            recoverService();
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover CardEmulationService.");
-                return null;
-            }
-            try {
-                return sService.getPreferredPaymentService(mContext.getUser().getIdentifier());
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to reach CardEmulationService.");
-                return null;
-            }
-        }
-    }
+    public static ComponentName getPreferredPaymentService(@NonNull Context context) {
+        context.checkCallingOrSelfPermission(Manifest.permission.NFC_PREFERRED_PAYMENT_INFO);
+        String defaultPaymentComponent = Settings.Secure.getString(context.getContentResolver(),
+                Constants.SETTINGS_SECURE_NFC_PAYMENT_DEFAULT_COMPONENT);
 
+        if (defaultPaymentComponent == null) {
+            return null;
+        }
+
+        return ComponentName.unflattenFromString(defaultPaymentComponent);
+    }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
index ba8e354..b34c310 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
@@ -54,16 +54,18 @@
     selectedOptionsState: SnapshotStateList<Int>,
     emptyVal: String = "",
     enabled: Boolean,
+    errorMessage: String? = null,
     onSelectedOptionStateChange: () -> Unit,
 ) {
     var dropDownWidth by remember { mutableIntStateOf(0) }
     var expanded by remember { mutableStateOf(false) }
+    val allIndex = options.indexOf("*")
     ExposedDropdownMenuBox(
         expanded = expanded,
         onExpandedChange = { expanded = it },
         modifier = Modifier
             .width(350.dp)
-            .padding(SettingsDimension.menuFieldPadding)
+            .padding(SettingsDimension.textFieldPadding)
             .onSizeChanged { dropDownWidth = it.width },
     ) {
         OutlinedTextField(
@@ -72,7 +74,8 @@
                 .menuAnchor()
                 .fillMaxWidth(),
             value = if (selectedOptionsState.size == 0) emptyVal
-                    else selectedOptionsState.joinToString { options[it] },
+            else if (selectedOptionsState.contains(allIndex)) "*"
+            else selectedOptionsState.joinToString { options[it] },
             onValueChange = {},
             label = { Text(text = label) },
             trailingIcon = {
@@ -81,7 +84,13 @@
                 )
             },
             readOnly = true,
-            enabled = enabled
+            enabled = enabled,
+            isError = errorMessage != null,
+            supportingText = {
+                if (errorMessage != null) {
+                    Text(text = errorMessage)
+                }
+            }
         )
         if (options.isNotEmpty()) {
             ExposedDropdownMenu(
@@ -98,9 +107,17 @@
                             .fillMaxWidth(),
                         onClick = {
                             if (selectedOptionsState.contains(index)) {
-                                selectedOptionsState.remove(
-                                    index
-                                )
+                                if (index == allIndex)
+                                    selectedOptionsState.clear()
+                                else {
+                                    selectedOptionsState.remove(
+                                        index
+                                    )
+                                    if (selectedOptionsState.contains(allIndex))
+                                        selectedOptionsState.remove(
+                                            allIndex
+                                        )
+                                }
                             } else {
                                 selectedOptionsState.add(
                                     index
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
index 5d520ce..7e2d0af 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
@@ -21,6 +21,8 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 
+import androidx.annotation.NonNull;
+
 /**
  * A class for applying config changes and determing if doing so resulting in any "interesting"
  * changes.
@@ -48,8 +50,15 @@
      */
     @SuppressLint("NewApi")
     public boolean applyNewConfig(Resources res) {
+        return applyNewConfig(res.getConfiguration());
+    }
+
+    /**
+     * Applies the given config change and returns whether an "interesting" change happened.
+     */
+    public boolean applyNewConfig(@NonNull Configuration configuration) {
         int configChanges = mLastConfiguration.updateFrom(
-                Configuration.generateDelta(mLastConfiguration, res.getConfiguration()));
+                Configuration.generateDelta(mLastConfiguration, configuration));
         return (configChanges & (mFlags)) != 0;
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
index 57012aa..931a6f1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -3,17 +3,17 @@
 */
 
 /* 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.
-*/
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
 package com.android.settingslib.bluetooth;
 
@@ -23,6 +23,7 @@
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothCsipSetCoordinator;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
@@ -30,6 +31,7 @@
 import android.os.Build;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
@@ -57,13 +59,12 @@
     private static final int ORDINAL = 1;
 
     // These callbacks run on the main thread.
-    private final class LeAudioServiceListener
-            implements BluetoothProfile.ServiceListener {
+    private final class LeAudioServiceListener implements BluetoothProfile.ServiceListener {
 
         @RequiresApi(Build.VERSION_CODES.S)
         public void onServiceConnected(int profile, BluetoothProfile proxy) {
             if (DEBUG) {
-                Log.d(TAG,"Bluetooth service connected");
+                Log.d(TAG, "Bluetooth service connected");
             }
             mService = (BluetoothLeAudio) proxy;
             // We just bound to the service, so refresh the UI for any connected LeAudio devices.
@@ -78,8 +79,7 @@
                     }
                     device = mDeviceManager.addDevice(nextDevice);
                 }
-                device.onProfileStateChanged(LeAudioProfile.this,
-                        BluetoothProfile.STATE_CONNECTED);
+                device.onProfileStateChanged(LeAudioProfile.this, BluetoothProfile.STATE_CONNECTED);
                 device.refresh();
             }
 
@@ -89,7 +89,7 @@
 
         public void onServiceDisconnected(int profile) {
             if (DEBUG) {
-                 Log.d(TAG,"Bluetooth service disconnected");
+                Log.d(TAG, "Bluetooth service disconnected");
             }
             mProfileManager.callServiceDisconnectedListeners();
             mIsProfileReady = false;
@@ -105,7 +105,9 @@
         return BluetoothProfile.LE_AUDIO;
     }
 
-    LeAudioProfile(Context context, CachedBluetoothDeviceManager deviceManager,
+    LeAudioProfile(
+            Context context,
+            CachedBluetoothDeviceManager deviceManager,
             LocalBluetoothProfileManager profileManager) {
         mContext = context;
         mDeviceManager = deviceManager;
@@ -113,8 +115,7 @@
 
         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
         mBluetoothAdapter.getProfileProxy(
-                context, new LeAudioServiceListener(),
-                BluetoothProfile.LE_AUDIO);
+                context, new LeAudioServiceListener(), BluetoothProfile.LE_AUDIO);
     }
 
     public boolean accessProfileEnabled() {
@@ -126,18 +127,22 @@
     }
 
     public List<BluetoothDevice> getConnectedDevices() {
-        return getDevicesByStates(new int[] {
-                BluetoothProfile.STATE_CONNECTED,
-                BluetoothProfile.STATE_CONNECTING,
-                BluetoothProfile.STATE_DISCONNECTING});
+        return getDevicesByStates(
+                new int[] {
+                    BluetoothProfile.STATE_CONNECTED,
+                    BluetoothProfile.STATE_CONNECTING,
+                    BluetoothProfile.STATE_DISCONNECTING
+                });
     }
 
     public List<BluetoothDevice> getConnectableDevices() {
-        return getDevicesByStates(new int[] {
-                BluetoothProfile.STATE_DISCONNECTED,
-                BluetoothProfile.STATE_CONNECTED,
-                BluetoothProfile.STATE_CONNECTING,
-                BluetoothProfile.STATE_DISCONNECTING});
+        return getDevicesByStates(
+                new int[] {
+                    BluetoothProfile.STATE_DISCONNECTED,
+                    BluetoothProfile.STATE_CONNECTED,
+                    BluetoothProfile.STATE_CONNECTING,
+                    BluetoothProfile.STATE_DISCONNECTING
+                });
     }
 
     private List<BluetoothDevice> getDevicesByStates(int[] states) {
@@ -148,8 +153,8 @@
     }
 
     /*
-    * @hide
-    */
+     * @hide
+     */
     public boolean connect(BluetoothDevice device) {
         if (mService == null) {
             return false;
@@ -158,8 +163,8 @@
     }
 
     /*
-    * @hide
-    */
+     * @hide
+     */
     public boolean disconnect(BluetoothDevice device) {
         if (mService == null) {
             return false;
@@ -174,6 +179,14 @@
         return mService.getConnectionState(device);
     }
 
+    /** Get group id for {@link BluetoothDevice}. */
+    public int getGroupId(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+        }
+        return mService.getGroupId(device);
+    }
+
     public boolean setActiveDevice(BluetoothDevice device) {
         if (mBluetoothAdapter == null) {
             return false;
@@ -193,29 +206,28 @@
     /**
      * Get Lead device for the group.
      *
-     * Lead device is the device that can be used as an active device in the system.
-     * Active devices points to the Audio Device for the Le Audio group.
-     * This method returns the Lead devices for the connected LE Audio
-     * group and this device should be used in the setActiveDevice() method by other parts
-     * of the system, which wants to set to active a particular Le Audio group.
+     * <p>Lead device is the device that can be used as an active device in the system. Active
+     * devices points to the Audio Device for the Le Audio group. This method returns the Lead
+     * devices for the connected LE Audio group and this device should be used in the
+     * setActiveDevice() method by other parts of the system, which wants to set to active a
+     * particular Le Audio group.
      *
-     * Note: getActiveDevice() returns the Lead device for the currently active LE Audio group.
+     * <p>Note: getActiveDevice() returns the Lead device for the currently active LE Audio group.
      * Note: When Lead device gets disconnected while Le Audio group is active and has more devices
-     * in the group, then Lead device will not change. If Lead device gets disconnected, for the
-     * Le Audio group which is not active, a new Lead device will be chosen
+     * in the group, then Lead device will not change. If Lead device gets disconnected, for the Le
+     * Audio group which is not active, a new Lead device will be chosen
      *
      * @param groupId The group id.
      * @return group lead device.
-     *
      * @hide
      */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) {
         if (DEBUG) {
-            Log.d(TAG,"getConnectedGroupLeadDevice");
+            Log.d(TAG, "getConnectedGroupLeadDevice");
         }
         if (mService == null) {
-            Log.e(TAG,"No service.");
+            Log.e(TAG, "No service.");
             return null;
         }
         return mService.getConnectedGroupLeadDevice(groupId);
@@ -310,10 +322,10 @@
         }
         if (mService != null) {
             try {
-                BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.LE_AUDIO,
-                        mService);
+                BluetoothAdapter.getDefaultAdapter()
+                        .closeProfileProxy(BluetoothProfile.LE_AUDIO, mService);
                 mService = null;
-            }catch (Throwable t) {
+            } catch (Throwable t) {
                 Log.w(TAG, "Error cleaning up LeAudio proxy", t);
             }
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index de21c54..9348705 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -84,6 +84,8 @@
                 Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO),
                 Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE),
                 Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME),
+                Settings.Secure.getUriFor(
+                        Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY),
             };
 
     private BluetoothLeBroadcast mServiceBroadcast;
@@ -96,6 +98,7 @@
     private String mNewAppSourceName = "";
     private boolean mIsBroadcastProfileReady = false;
     private boolean mIsBroadcastAssistantProfileReady = false;
+    private boolean mImproveCompatibility = false;
     private String mProgramInfo;
     private byte[] mBroadcastCode;
     private Executor mExecutor;
@@ -391,6 +394,52 @@
      * <p>If the system started the LE Broadcast, then the system calls the corresponding callback
      * {@link BluetoothLeBroadcast.Callback}.
      */
+    public void startPrivateBroadcast() {
+        mNewAppSourceName = "Sharing audio";
+        if (mServiceBroadcast == null) {
+            Log.d(TAG, "The BluetoothLeBroadcast is null when starting the private broadcast.");
+            return;
+        }
+        if (mServiceBroadcast.getAllBroadcastMetadata().size()
+                >= mServiceBroadcast.getMaximumNumberOfBroadcasts()) {
+            Log.d(TAG, "Skip starting the broadcast due to number limit.");
+            return;
+        }
+        String programInfo = getProgramInfo();
+        boolean improveCompatibility = getImproveCompatibility();
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "startBroadcast: language = null , programInfo = "
+                            + programInfo
+                            + ", improveCompatibility = "
+                            + improveCompatibility);
+        }
+        // Current broadcast framework only support one subgroup
+        BluetoothLeBroadcastSubgroupSettings subgroupSettings =
+                buildBroadcastSubgroupSettings(
+                        /* language= */ null, programInfo, improveCompatibility);
+        BluetoothLeBroadcastSettings settings =
+                buildBroadcastSettings(
+                        true, // TODO: set to false after framework fix
+                        TextUtils.isEmpty(programInfo) ? null : programInfo,
+                        (mBroadcastCode != null && mBroadcastCode.length > 0)
+                                ? mBroadcastCode
+                                : null,
+                        ImmutableList.of(subgroupSettings));
+        mServiceBroadcast.startBroadcast(settings);
+    }
+
+    /**
+     * Start the private Broadcast for personal audio sharing or qr code sharing.
+     *
+     * <p>The broadcast will use random string for both broadcast name and subgroup program info;
+     * The broadcast will use random string for broadcast code; The broadcast will only have one
+     * subgroup due to system limitation; The subgroup language will be null.
+     *
+     * <p>If the system started the LE Broadcast, then the system calls the corresponding callback
+     * {@link BluetoothLeBroadcast.Callback}.
+     */
     public void startPrivateBroadcast(int quality) {
         mNewAppSourceName = "Sharing audio";
         if (mServiceBroadcast == null) {
@@ -408,7 +457,11 @@
         }
         // Current broadcast framework only support one subgroup
         BluetoothLeBroadcastSubgroupSettings subgroupSettings =
-                buildBroadcastSubgroupSettings(/* language= */ null, programInfo, quality);
+                buildBroadcastSubgroupSettings(
+                        /* language= */ null,
+                        programInfo,
+                        /* improveCompatibility= */
+                        BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD == quality);
         BluetoothLeBroadcastSettings settings =
                 buildBroadcastSettings(
                         true, // TODO: set to false after framework fix
@@ -437,7 +490,7 @@
     }
 
     private BluetoothLeBroadcastSubgroupSettings buildBroadcastSubgroupSettings(
-            @Nullable String language, @Nullable String programInfo, int quality) {
+            @Nullable String language, @Nullable String programInfo, boolean improveCompatibility) {
         BluetoothLeAudioContentMetadata metadata =
                 new BluetoothLeAudioContentMetadata.Builder()
                         .setLanguage(language)
@@ -447,7 +500,10 @@
         // metadata to keep legacy UI working.
         mBluetoothLeAudioContentMetadata = metadata;
         return new BluetoothLeBroadcastSubgroupSettings.Builder()
-                .setPreferredQuality(quality)
+                .setPreferredQuality(
+                        improveCompatibility
+                                ? BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD
+                                : BluetoothLeBroadcastSubgroupSettings.QUALITY_HIGH)
                 .setContentMetadata(mBluetoothLeAudioContentMetadata)
                 .build();
     }
@@ -513,6 +569,36 @@
         }
     }
 
+    /** Get compatibility config for broadcast. */
+    public boolean getImproveCompatibility() {
+        return mImproveCompatibility;
+    }
+
+    /** Set compatibility config for broadcast. */
+    public void setImproveCompatibility(boolean improveCompatibility) {
+        setImproveCompatibility(improveCompatibility, /* updateContentResolver= */ true);
+    }
+
+    private void setImproveCompatibility(
+            boolean improveCompatibility, boolean updateContentResolver) {
+        if (mImproveCompatibility == improveCompatibility) {
+            Log.d(TAG, "setImproveCompatibility: improveCompatibility is not changed");
+            return;
+        }
+        mImproveCompatibility = improveCompatibility;
+        if (updateContentResolver) {
+            if (mContentResolver == null) {
+                Log.d(TAG, "mContentResolver is null");
+                return;
+            }
+            Log.d(TAG, "Set improveCompatibility to: " + improveCompatibility);
+            Settings.Secure.putString(
+                    mContentResolver,
+                    Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY,
+                    improveCompatibility ? "1" : "0");
+        }
+    }
+
     private void setLatestBroadcastId(int broadcastId) {
         Log.d(TAG, "setLatestBroadcastId: mBroadcastId is " + broadcastId);
         mBroadcastId = broadcastId;
@@ -600,6 +686,14 @@
                 Settings.Secure.getString(
                         mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME);
         setAppSourceName(appSourceName, /* updateContentResolver= */ false);
+
+        String improveCompatibility =
+                Settings.Secure.getString(
+                        mContentResolver,
+                        Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY);
+        setImproveCompatibility(
+                improveCompatibility == null ? false : improveCompatibility.equals("1"),
+                /* updateContentResolver= */ false);
     }
 
     private void updateBroadcastInfoFromBroadcastMetadata(
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index d6f1eab..15f33d2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -229,6 +229,17 @@
     @SuppressWarnings("NewApi")
     @Override
     public String getId() {
+        if (com.android.media.flags.Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+            // Note: be careful when removing this flag. Instead of just removing it, you might want
+            // to replace it with SDK_INT >= 35. Explanation: The presence of SDK checks in settings
+            // lib suggests that a mainline component may depend on this code. Which means removing
+            // this "if" (and using always the route info id) could mean a regression on mainline
+            // code running on a device that's running API 34 or older. Unfortunately, we cannot
+            // check the API level at the moment of writing this code because the API level has not
+            // been bumped, yet.
+            return mRouteInfo.getId();
+        }
+
         String id;
         switch (mRouteInfo.getType()) {
             case TYPE_WIRED_HEADSET:
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
index 1746bef..ceba9be 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
@@ -31,10 +31,15 @@
 
 import android.content.Context;
 import android.media.MediaRoute2Info;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 
+import com.android.media.flags.Flags;
 import com.android.settingslib.R;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -45,6 +50,8 @@
 @RunWith(RobolectricTestRunner.class)
 public class PhoneMediaDeviceTest {
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Mock
     private MediaRoute2Info mInfo;
 
@@ -110,8 +117,18 @@
                 .isEqualTo(mContext.getString(R.string.media_transfer_this_device_name));
     }
 
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
     @Test
-    public void getId_returnCorrectId() {
+    public void getId_whenAdvancedWiredRoutingEnabled_returnCorrectId() {
+        String fakeId = "foo";
+        when(mInfo.getId()).thenReturn(fakeId);
+
+        assertThat(mPhoneMediaDevice.getId()).isEqualTo(fakeId);
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
+    @Test
+    public void getId_whenAdvancedWiredRoutingDisabled_returnCorrectId() {
         when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES);
 
         assertThat(mPhoneMediaDevice.getId())
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 2d442f4..3a46f4e 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -406,7 +406,7 @@
                     Process.THREAD_PRIORITY_BACKGROUND);
             mHandlerThread.start();
             mHandler = new Handler(mHandlerThread.getLooper());
-            mSettingsRegistry = new SettingsRegistry();
+            mSettingsRegistry = new SettingsRegistry(mHandlerThread.getLooper());
         }
         SettingsState.cacheSystemPackageNamesAndSystemSignature(getContext());
         synchronized (mLock) {
@@ -2896,8 +2896,8 @@
 
         private String mSettingsCreationBuildId;
 
-        public SettingsRegistry() {
-            mHandler = new MyHandler(getContext().getMainLooper());
+        SettingsRegistry(Looper looper) {
+            mHandler = new MyHandler(looper);
             mGenerationRegistry = new GenerationRegistry(UserManager.getMaxSupportedUsers());
             mBackupManager = new BackupManager(getContext());
         }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d61ae7e..80656e9 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -361,6 +361,7 @@
         "androidx.test.ext.junit",
         "androidx.test.ext.truth",
         "kotlin-test",
+        "SystemUICustomizationTestUtils",
     ],
     libs: [
         "android.test.runner",
@@ -439,6 +440,7 @@
         "androidx.test.ext.junit",
         "inline-mockito-robolectric-prebuilt",
         "platform-parametric-runner-lib",
+        "SystemUICustomizationTestUtils",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3236130..2c35c77 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -89,6 +89,13 @@
 }
 
 flag {
+    name: "notification_avalanche_suppression"
+    namespace: "systemui"
+    description: "After notification avalanche floodgate event, suppress HUNs completely."
+    bug: "321089634"
+}
+
+flag {
     name: "notification_background_tint_optimization"
     namespace: "systemui"
     description: "Re-enable the codepath that removed tinting of notifications when the"
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 2052e2c..a7557d8 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.compose
 
+import android.app.Dialog
 import android.content.Context
 import android.view.View
 import android.view.WindowInsets
@@ -26,11 +27,13 @@
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.scene.shared.model.Scene
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.StateFlow
 
@@ -78,6 +81,13 @@
         throwComposeUnavailableError()
     }
 
+    override fun createStickyKeysDialog(
+        dialogFactory: SystemUIDialogFactory,
+        viewModel: StickyKeysIndicatorViewModel
+    ): Dialog {
+        throwComposeUnavailableError()
+    }
+
     override fun createCommunalView(
         context: Context,
         viewModel: BaseCommunalViewModel,
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index b607d59..d63939d 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.compose
 
+import android.app.Dialog
 import android.content.Context
 import android.graphics.Point
 import android.view.View
@@ -38,6 +39,8 @@
 import com.android.systemui.communal.ui.compose.CommunalHub
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.keyboard.stickykeys.ui.view.StickyKeysIndicator
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
 import com.android.systemui.people.ui.compose.PeopleScreen
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -47,6 +50,8 @@
 import com.android.systemui.scene.ui.composable.ComposableScene
 import com.android.systemui.scene.ui.composable.SceneContainer
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.statusbar.phone.create
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -120,6 +125,13 @@
         }
     }
 
+    override fun createStickyKeysDialog(
+        dialogFactory: SystemUIDialogFactory,
+        viewModel: StickyKeysIndicatorViewModel
+    ): Dialog {
+        return dialogFactory.create { StickyKeysIndicator(viewModel) }
+    }
+
     override fun createCommunalView(
         context: Context,
         viewModel: BaseCommunalViewModel,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt
new file mode 100644
index 0000000..68e57b5
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.stickykeys.ui.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.android.systemui.keyboard.stickykeys.shared.model.Locked
+import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+
+@Composable
+fun StickyKeysIndicator(viewModel: StickyKeysIndicatorViewModel) {
+    val stickyKeys by viewModel.indicatorContent.collectAsState(emptyMap())
+    StickyKeysIndicator(stickyKeys)
+}
+
+@Composable
+fun StickyKeysIndicator(stickyKeys: Map<ModifierKey, Locked>, modifier: Modifier = Modifier) {
+    Surface(
+        color = MaterialTheme.colorScheme.surface,
+        shape = MaterialTheme.shapes.medium,
+        modifier = modifier
+    ) {
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
+            modifier = Modifier.padding(16.dp)
+        ) {
+            stickyKeys.forEach { (key, isLocked) ->
+                key(key) {
+                    Text(
+                        text = key.text,
+                        fontWeight = if (isLocked.locked) FontWeight.Bold else FontWeight.Normal
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 9778e53..c027c49 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -16,17 +16,16 @@
 
 package com.android.systemui.qs.ui.composable
 
-import android.view.ContextThemeWrapper
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
@@ -53,14 +52,6 @@
     }
 }
 
-@Composable
-private fun QuickSettingsTheme(content: @Composable () -> Unit) {
-    val context = LocalContext.current
-    val themedContext =
-        remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) }
-    CompositionLocalProvider(LocalContext provides themedContext) { content() }
-}
-
 private fun SceneScope.stateForQuickSettingsContent(): QSSceneAdapter.State {
     return when (val transitionState = layoutState.transitionState) {
         is TransitionState.Idle -> {
@@ -115,6 +106,7 @@
     modifier: Modifier = Modifier,
 ) {
     val qsView by qsSceneAdapter.qsView.collectAsState(null)
+    val isCustomizing by qsSceneAdapter.isCustomizing.collectAsState()
     QuickSettingsTheme {
         val context = LocalContext.current
 
@@ -124,14 +116,27 @@
             }
         }
         qsView?.let { view ->
-            AndroidView(
-                modifier = modifier.fillMaxSize().background(colorAttr(R.attr.underSurface)),
-                factory = { _ ->
-                    qsSceneAdapter.setState(state)
-                    view
-                },
-                update = { qsSceneAdapter.setState(state) }
-            )
+            Box(
+                modifier =
+                    modifier
+                        .fillMaxWidth()
+                        .then(
+                            if (isCustomizing) {
+                                Modifier.fillMaxHeight()
+                            } else {
+                                Modifier.wrapContentHeight()
+                            }
+                        )
+            ) {
+                AndroidView(
+                    modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)),
+                    factory = { _ ->
+                        qsSceneAdapter.setState(state)
+                        view
+                    },
+                    update = { qsSceneAdapter.setState(state) }
+                )
+            }
         }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index d8c7290..bbfe0fd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -24,31 +24,44 @@
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.shrinkVertically
 import androidx.compose.foundation.background
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.TransitionState
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
+import com.android.systemui.qs.footer.ui.compose.FooterActions
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.scene.ui.composable.toTransitionSceneKey
 import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
 import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
 import com.android.systemui.shade.ui.composable.Shade
@@ -105,57 +118,120 @@
 ) {
     // TODO(b/280887232): implement the real UI.
     Box(modifier = modifier.fillMaxSize()) {
-        Box(modifier = Modifier.fillMaxSize()) {
-            val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
-            val collapsedHeaderHeight =
-                with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
-            Spacer(
-                modifier =
-                    Modifier.element(Shade.Elements.ScrimBackground)
-                        .fillMaxSize()
-                        .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
-            )
-            Column(
-                horizontalAlignment = Alignment.CenterHorizontally,
-                modifier =
-                    Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp, bottom = 48.dp)
-            ) {
-                when (LocalWindowSizeClass.current.widthSizeClass) {
-                    WindowWidthSizeClass.Compact ->
-                        AnimatedVisibility(
-                            visible = !isCustomizing,
-                            enter =
-                                expandVertically(
-                                    animationSpec = tween(1000),
-                                    initialHeight = { collapsedHeaderHeight },
-                                ) + fadeIn(tween(1000)),
-                            exit =
-                                shrinkVertically(
-                                    animationSpec = tween(1000),
-                                    targetHeight = { collapsedHeaderHeight },
-                                    shrinkTowards = Alignment.Top,
-                                ) + fadeOut(tween(1000)),
-                        ) {
-                            ExpandedShadeHeader(
+        val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+        val collapsedHeaderHeight =
+            with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
+        val lifecycleOwner = LocalLifecycleOwner.current
+        val footerActionsViewModel =
+            remember(lifecycleOwner, viewModel) {
+                viewModel.getFooterActionsViewModel(lifecycleOwner)
+            }
+        val scrollState = rememberScrollState()
+        // When animating into the scene, we don't want it to be able to scroll, as it could mess
+        // up with the expansion animation.
+        val isScrollable =
+            when (val state = layoutState.transitionState) {
+                is TransitionState.Idle -> true
+                is TransitionState.Transition -> {
+                    state.fromScene == SceneKey.QuickSettings.toTransitionSceneKey()
+                }
+            }
+
+        LaunchedEffect(isCustomizing, scrollState) {
+            if (isCustomizing) {
+                scrollState.scrollTo(0)
+            }
+        }
+
+        // This is the background for the whole scene, as the elements don't necessarily provide
+        // a background that extends to the edges.
+        Spacer(
+            modifier =
+                Modifier.element(Shade.Elements.ScrimBackground)
+                    .fillMaxSize()
+                    .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
+        )
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
+            modifier =
+                Modifier.fillMaxSize()
+                    // bottom should be tied to insets
+                    .padding(bottom = 16.dp)
+        ) {
+            Box(modifier = Modifier.fillMaxSize().weight(1f)) {
+                val shadeHeaderAndQuickSettingsModifier =
+                    if (isCustomizing) {
+                        Modifier.fillMaxHeight().align(Alignment.TopCenter)
+                    } else {
+                        Modifier.verticalNestedScrollToScene()
+                            .verticalScroll(
+                                scrollState,
+                                enabled = isScrollable,
+                            )
+                            .clipScrollableContainer(Orientation.Horizontal)
+                            .fillMaxWidth()
+                            .wrapContentHeight(unbounded = true)
+                            .align(Alignment.TopCenter)
+                    }
+
+                Column(
+                    modifier = shadeHeaderAndQuickSettingsModifier,
+                ) {
+                    when (LocalWindowSizeClass.current.widthSizeClass) {
+                        WindowWidthSizeClass.Compact ->
+                            AnimatedVisibility(
+                                visible = !isCustomizing,
+                                enter =
+                                    expandVertically(
+                                        animationSpec = tween(100),
+                                        initialHeight = { collapsedHeaderHeight },
+                                    ) + fadeIn(tween(100)),
+                                exit =
+                                    shrinkVertically(
+                                        animationSpec = tween(100),
+                                        targetHeight = { collapsedHeaderHeight },
+                                        shrinkTowards = Alignment.Top,
+                                    ) + fadeOut(tween(100)),
+                            ) {
+                                ExpandedShadeHeader(
+                                    viewModel = viewModel.shadeHeaderViewModel,
+                                    createTintedIconManager = createTintedIconManager,
+                                    createBatteryMeterViewController =
+                                        createBatteryMeterViewController,
+                                    statusBarIconController = statusBarIconController,
+                                    modifier = Modifier.padding(horizontal = 16.dp),
+                                )
+                            }
+                        else ->
+                            CollapsedShadeHeader(
                                 viewModel = viewModel.shadeHeaderViewModel,
                                 createTintedIconManager = createTintedIconManager,
                                 createBatteryMeterViewController = createBatteryMeterViewController,
                                 statusBarIconController = statusBarIconController,
+                                modifier = Modifier.padding(horizontal = 16.dp),
                             )
-                        }
-                    else ->
-                        CollapsedShadeHeader(
-                            viewModel = viewModel.shadeHeaderViewModel,
-                            createTintedIconManager = createTintedIconManager,
-                            createBatteryMeterViewController = createBatteryMeterViewController,
-                            statusBarIconController = statusBarIconController,
-                        )
+                    }
+                    Spacer(modifier = Modifier.height(16.dp))
+                    // This view has its own horizontal padding
+                    QuickSettings(
+                        modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
+                        viewModel.qsSceneAdapter,
+                    )
                 }
-                Spacer(modifier = Modifier.height(16.dp))
-                QuickSettings(
-                    modifier = Modifier.fillMaxHeight(),
-                    viewModel.qsSceneAdapter,
-                )
+            }
+            AnimatedVisibility(
+                visible = !isCustomizing,
+                modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()
+            ) {
+                QuickSettingsTheme {
+                    // This view has its own horizontal padding
+                    // TODO(b/321716470) This should use a lifecycle tied to the scene.
+                    FooterActions(
+                        viewModel = footerActionsViewModel,
+                        qsVisibilityLifecycleOwner = lifecycleOwner,
+                        modifier = Modifier.element(QuickSettings.Elements.FooterActions)
+                    )
+                }
             }
         }
         HeadsUpNotificationSpace(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt
new file mode 100644
index 0000000..87b6f95b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.ui.composable
+
+import android.view.ContextThemeWrapper
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import com.android.systemui.res.R
+
+@Composable
+fun QuickSettingsTheme(content: @Composable () -> Unit) {
+    val context = LocalContext.current
+    val themedContext =
+        remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) }
+    CompositionLocalProvider(LocalContext provides themedContext) { content() }
+}
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
index 1d18496..81b5bd4 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -34,15 +34,19 @@
         "PluginCoreLib",
         "SystemUIPluginLib",
         "SystemUIUnfoldLib",
-        "androidx.dynamicanimation_dynamicanimation",
+        "kotlinx_coroutines",
+        "dagger2",
+        "jsr330",
+    ],
+    libs: [
+        // Keep android-specific libraries as libs instead of static_libs, so that they don't break
+        // things when included as transitive dependencies in robolectric targets.
         "androidx.concurrent_concurrent-futures",
+        "androidx.dynamicanimation_dynamicanimation",
         "androidx.lifecycle_lifecycle-runtime-ktx",
         "androidx.lifecycle_lifecycle-viewmodel-ktx",
         "androidx.recyclerview_recyclerview",
         "kotlinx_coroutines_android",
-        "kotlinx_coroutines",
-        "dagger2",
-        "jsr330",
     ],
     resource_dirs: [
         "res",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt
index ccf119a..97c407c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt
@@ -41,10 +41,10 @@
 class FingerprintPropertyInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val underTest = kosmos.fingerprintPropertyInteractor
-    private val repository = kosmos.fingerprintPropertyRepository
-    private val configurationRepository = kosmos.fakeConfigurationRepository
-    private val displayRepository = kosmos.displayRepository
+    private val underTest by lazy { kosmos.fingerprintPropertyInteractor }
+    private val repository by lazy { kosmos.fingerprintPropertyRepository }
+    private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
+    private val displayRepository by lazy { kosmos.displayRepository }
 
     @Test
     fun sensorLocation_resolution1f() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
index c8560c3..c300e0a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
@@ -55,7 +55,9 @@
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
     @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor
-    private val mainHandler = FakeHandler(Looper.getMainLooper())
+    private val mainHandler by lazy {
+        FakeHandler(Looper.getMainLooper())
+    }
     private lateinit var underTest: PrimaryBouncerInteractor
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index 27b84b2..d30e333 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -39,8 +39,8 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val bouncerInteractor = kosmos.bouncerInteractor
-    private val underTest =
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private val underTest by lazy {
         PinBouncerViewModel(
             applicationContext = context,
             viewModelScope = testScope.backgroundScope,
@@ -49,6 +49,7 @@
             simBouncerInteractor = kosmos.simBouncerInteractor,
             authenticationMethod = AuthenticationMethodModel.Pin,
         )
+    }
 
     @Test
     fun animateFailure() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index cfe8c5d..73db175 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -58,8 +58,8 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val authenticationInteractor = kosmos.authenticationInteractor
-    private val bouncerInteractor = kosmos.bouncerInteractor
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
     private lateinit var underTest: BouncerViewModel
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
index a0c2acc..cddbd1f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
@@ -66,7 +66,9 @@
     @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor
 
     lateinit var bouncerInteractor: PrimaryBouncerInteractor
-    private val mainHandler = FakeHandler(Looper.getMainLooper())
+    private val mainHandler by lazy {
+        FakeHandler(Looper.getMainLooper())
+    }
     val repository = FakeKeyguardBouncerRepository()
 
     lateinit var underTest: KeyguardBouncerViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index b3b6457..c6d612d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -52,17 +52,18 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val authenticationInteractor = kosmos.authenticationInteractor
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val bouncerInteractor = kosmos.bouncerInteractor
-    private val bouncerViewModel = kosmos.bouncerViewModel
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
     private val isInputEnabled = MutableStateFlow(true)
 
-    private val underTest =
+    private val underTest by lazy {
         PasswordBouncerViewModel(
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled.asStateFlow(),
         )
+    }
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index c2680bc..725bdbd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -53,17 +53,18 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val authenticationInteractor = kosmos.authenticationInteractor
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val bouncerInteractor = kosmos.bouncerInteractor
-    private val bouncerViewModel = kosmos.bouncerViewModel
-    private val underTest =
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
+    private val underTest by lazy {
         PatternBouncerViewModel(
             applicationContext = context,
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
         )
+    }
 
     private val containerSize = 90 // px
     private val dotSize = 30 // px
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 1d660d6..06e1258 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -53,22 +53,24 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val authenticationInteractor = kosmos.authenticationInteractor
-    private val bouncerInteractor = kosmos.bouncerInteractor
-    private val bouncerViewModel = kosmos.bouncerViewModel
-    private val underTest =
-        PinBouncerViewModel(
-            applicationContext = context,
-            viewModelScope = testScope.backgroundScope,
-            interactor = bouncerInteractor,
-            isInputEnabled = MutableStateFlow(true).asStateFlow(),
-            simBouncerInteractor = kosmos.simBouncerInteractor,
-            authenticationMethod = AuthenticationMethodModel.Pin,
-        )
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
+    private lateinit var underTest: PinBouncerViewModel
 
     @Before
     fun setUp() {
+        underTest =
+            PinBouncerViewModel(
+                    applicationContext = context,
+                    viewModelScope = testScope.backgroundScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = MutableStateFlow(true).asStateFlow(),
+                    simBouncerInteractor = kosmos.simBouncerInteractor,
+                    authenticationMethod = AuthenticationMethodModel.Pin,
+            )
+
         overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN)
         overrideResource(R.string.kg_wrong_pin, WRONG_PIN)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
index ed29aa4..e821673 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.communalInteractor
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 2119060..86279ef 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.communal.domain.interactor
 
 import android.app.smartspace.SmartspaceTarget
+import android.content.pm.UserInfo
 import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
 import android.widget.RemoteViews
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,12 +43,12 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.communalInteractor
-import com.android.systemui.keyguard.domain.interactor.editWidgetsActivityStarter
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
 import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -59,8 +60,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
 
 /**
  * This class of test cases assume that communal is enabled. For disabled cases, see
@@ -70,6 +73,9 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 class CommunalInteractorTest : SysuiTestCase() {
+    @Mock private lateinit var mainUser: UserInfo
+    @Mock private lateinit var secondaryUser: UserInfo
+
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
@@ -78,6 +84,7 @@
     private lateinit var mediaRepository: FakeCommunalMediaRepository
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
     private lateinit var smartspaceRepository: FakeSmartspaceRepository
+    private lateinit var userRepository: FakeUserRepository
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository
     private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter
@@ -86,15 +93,22 @@
 
     @Before
     fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
         tutorialRepository = kosmos.fakeCommunalTutorialRepository
         communalRepository = kosmos.fakeCommunalRepository
         mediaRepository = kosmos.fakeCommunalMediaRepository
         widgetRepository = kosmos.fakeCommunalWidgetRepository
         smartspaceRepository = kosmos.fakeSmartspaceRepository
+        userRepository = kosmos.fakeUserRepository
         keyguardRepository = kosmos.fakeKeyguardRepository
         editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter
         communalPrefsRepository = kosmos.fakeCommunalPrefsRepository
 
+        whenever(mainUser.isMain).thenReturn(true)
+        whenever(secondaryUser.isMain).thenReturn(false)
+        userRepository.setUserInfos(listOf(mainUser, secondaryUser))
+
         underTest = kosmos.communalInteractor
     }
 
@@ -103,36 +117,52 @@
         testScope.runTest { assertThat(underTest.isCommunalEnabled).isTrue() }
 
     @Test
-    fun isCommunalAvailable_trueWhenStorageUnlock() =
+    fun isCommunalAvailable_storageUnlockedAndMainUser_true() =
         testScope.runTest {
             val isAvailable by collectLastValue(underTest.isCommunalAvailable)
             assertThat(isAvailable).isFalse()
 
             keyguardRepository.setIsEncryptedOrLockdown(false)
+            userRepository.setSelectedUserInfo(mainUser)
             runCurrent()
 
             assertThat(isAvailable).isTrue()
         }
 
     @Test
-    fun isCommunalAvailable_whenStorageUnlock_true() =
+    fun isCommunalAvailable_storageLockedAndMainUser_false() =
+        testScope.runTest {
+            val isAvailable by collectLastValue(underTest.isCommunalAvailable)
+            assertThat(isAvailable).isFalse()
+
+            keyguardRepository.setIsEncryptedOrLockdown(true)
+            userRepository.setSelectedUserInfo(mainUser)
+            runCurrent()
+
+            assertThat(isAvailable).isFalse()
+        }
+
+    @Test
+    fun isCommunalAvailable_storageUnlockedAndSecondaryUser_false() =
         testScope.runTest {
             val isAvailable by collectLastValue(underTest.isCommunalAvailable)
             assertThat(isAvailable).isFalse()
 
             keyguardRepository.setIsEncryptedOrLockdown(false)
+            userRepository.setSelectedUserInfo(secondaryUser)
             runCurrent()
 
-            assertThat(isAvailable).isTrue()
+            assertThat(isAvailable).isFalse()
         }
 
     @Test
-    fun updateAppWidgetHostActive_uponStorageUnlock_true() =
+    fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() =
         testScope.runTest {
             collectLastValue(underTest.isCommunalAvailable)
             assertThat(widgetRepository.isHostActive()).isFalse()
 
             keyguardRepository.setIsEncryptedOrLockdown(false)
+            userRepository.setSelectedUserInfo(mainUser)
             runCurrent()
 
             assertThat(widgetRepository.isHostActive()).isTrue()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
index 161356d..352463f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.domain.interactor
 
+import android.content.pm.UserInfo
 import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
 import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
 import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_STARTED
@@ -30,9 +31,9 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.settings.UserTracker
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -45,15 +46,17 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class CommunalTutorialInteractorTest : SysuiTestCase() {
+    @Mock lateinit var user: UserInfo
+
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
-    @Mock private lateinit var userTracker: UserTracker
-
     private lateinit var underTest: CommunalTutorialInteractor
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var communalTutorialRepository: FakeCommunalTutorialRepository
     private lateinit var communalRepository: FakeCommunalRepository
+    private lateinit var communalInteractor: CommunalInteractor
+    private lateinit var userRepository: FakeUserRepository
 
     @Before
     fun setUp() {
@@ -62,16 +65,17 @@
         keyguardRepository = kosmos.fakeKeyguardRepository
         communalTutorialRepository = kosmos.fakeCommunalTutorialRepository
         communalRepository = kosmos.fakeCommunalRepository
+        communalInteractor = kosmos.communalInteractor
+        userRepository = kosmos.fakeUserRepository
 
         underTest = kosmos.communalTutorialInteractor
-
-        whenever(userTracker.userHandle).thenReturn(mock())
     }
 
     @Test
     fun tutorialUnavailable_whenKeyguardNotVisible() =
         testScope.runTest {
             val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+            setCommunalAvailable(true)
             communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
             keyguardRepository.setKeyguardShowing(false)
             assertThat(isTutorialAvailable).isFalse()
@@ -81,6 +85,7 @@
     fun tutorialUnavailable_whenTutorialIsCompleted() =
         testScope.runTest {
             val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+            setCommunalAvailable(true)
             keyguardRepository.setKeyguardShowing(true)
             keyguardRepository.setKeyguardOccluded(false)
             communalRepository.setIsCommunalHubShowing(false)
@@ -89,9 +94,20 @@
         }
 
     @Test
+    fun tutorialUnavailable_whenCommunalNotAvailable() =
+        testScope.runTest {
+            val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+            setCommunalAvailable(false)
+            communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
+            keyguardRepository.setKeyguardShowing(true)
+            assertThat(isTutorialAvailable).isFalse()
+        }
+
+    @Test
     fun tutorialAvailable_whenTutorialNotStarted() =
         testScope.runTest {
             val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+            setCommunalAvailable(true)
             keyguardRepository.setKeyguardShowing(true)
             keyguardRepository.setKeyguardOccluded(false)
             communalRepository.setIsCommunalHubShowing(false)
@@ -103,6 +119,7 @@
     fun tutorialAvailable_whenTutorialIsStarted() =
         testScope.runTest {
             val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+            setCommunalAvailable(true)
             keyguardRepository.setKeyguardShowing(true)
             keyguardRepository.setKeyguardOccluded(false)
             communalRepository.setIsCommunalHubShowing(true)
@@ -183,4 +200,16 @@
 
             assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED)
         }
+
+    private suspend fun setCommunalAvailable(available: Boolean) {
+        if (available) {
+            communalRepository.setIsCommunalEnabled(true)
+            keyguardRepository.setIsEncryptedOrLockdown(false)
+            whenever(user.isMain).thenReturn(true)
+            userRepository.setUserInfos(listOf(user))
+            userRepository.setSelectedUserInfo(user)
+        } else {
+            keyguardRepository.setIsEncryptedOrLockdown(true)
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt
index 721fc49..6b1b937 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt
@@ -21,14 +21,15 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -47,17 +48,16 @@
 class CommunalLoggerStartableTest : SysuiTestCase() {
     @Mock private lateinit var uiEventLogger: UiEventLogger
 
-    private lateinit var testScope: TestScope
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
     private lateinit var communalInteractor: CommunalInteractor
     private lateinit var underTest: CommunalLoggerStartable
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-
-        val withDeps = CommunalInteractorFactory.create()
-        testScope = withDeps.testScope
-        communalInteractor = withDeps.communalInteractor
+        communalInteractor = kosmos.communalInteractor
 
         underTest =
             CommunalLoggerStartable(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 867a48d..a2dec5f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -24,19 +24,21 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
+import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
+import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
+import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -59,8 +61,6 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
-    private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var communalRepository: FakeCommunalRepository
     private lateinit var tutorialRepository: FakeCommunalTutorialRepository
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
     private lateinit var smartspaceRepository: FakeSmartspaceRepository
@@ -72,17 +72,14 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        val withDeps = CommunalInteractorFactory.create(testScope)
-        keyguardRepository = withDeps.keyguardRepository
-        communalRepository = withDeps.communalRepository
-        tutorialRepository = withDeps.tutorialRepository
-        widgetRepository = withDeps.widgetRepository
-        smartspaceRepository = withDeps.smartspaceRepository
-        mediaRepository = withDeps.mediaRepository
+        tutorialRepository = kosmos.fakeCommunalTutorialRepository
+        widgetRepository = kosmos.fakeCommunalWidgetRepository
+        smartspaceRepository = kosmos.fakeSmartspaceRepository
+        mediaRepository = kosmos.fakeCommunalMediaRepository
 
         underTest =
             CommunalEditModeViewModel(
-                withDeps.communalInteractor,
+                kosmos.communalInteractor,
                 mediaHost,
                 uiEventLogger,
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 9ac21dd..c814f3f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -17,31 +17,39 @@
 package com.android.systemui.communal.view.viewmodel
 
 import android.app.smartspace.SmartspaceTarget
+import android.content.pm.UserInfo
 import android.provider.Settings
 import android.widget.RemoteViews
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
-import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
+import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
+import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalTutorialInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
+import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -57,16 +65,17 @@
 @RunWith(AndroidJUnit4::class)
 class CommunalViewModelTest : SysuiTestCase() {
     @Mock private lateinit var mediaHost: MediaHost
+    @Mock private lateinit var user: UserInfo
 
-    private lateinit var testScope: TestScope
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var communalRepository: FakeCommunalRepository
     private lateinit var tutorialRepository: FakeCommunalTutorialRepository
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
     private lateinit var smartspaceRepository: FakeSmartspaceRepository
     private lateinit var mediaRepository: FakeCommunalMediaRepository
-    private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository
+    private lateinit var userRepository: FakeUserRepository
 
     private lateinit var underTest: CommunalViewModel
 
@@ -74,22 +83,18 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        testScope = TestScope()
-
-        val withDeps = CommunalInteractorFactory.create()
-        keyguardRepository = withDeps.keyguardRepository
-        communalRepository = withDeps.communalRepository
-        tutorialRepository = withDeps.tutorialRepository
-        widgetRepository = withDeps.widgetRepository
-        smartspaceRepository = withDeps.smartspaceRepository
-        mediaRepository = withDeps.mediaRepository
-        communalPrefsRepository = withDeps.communalPrefsRepository
+        keyguardRepository = kosmos.fakeKeyguardRepository
+        tutorialRepository = kosmos.fakeCommunalTutorialRepository
+        widgetRepository = kosmos.fakeCommunalWidgetRepository
+        smartspaceRepository = kosmos.fakeSmartspaceRepository
+        mediaRepository = kosmos.fakeCommunalMediaRepository
+        userRepository = kosmos.fakeUserRepository
 
         underTest =
             CommunalViewModel(
                 testScope,
-                withDeps.communalInteractor,
-                withDeps.tutorialInteractor,
+                kosmos.communalInteractor,
+                kosmos.communalTutorialInteractor,
                 mediaHost,
             )
     }
@@ -104,9 +109,11 @@
     @Test
     fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() =
         testScope.runTest {
-            // Keyguard showing, and tutorial not started.
+            // Keyguard showing, storage unlocked, main user, and tutorial not started.
             keyguardRepository.setKeyguardShowing(true)
             keyguardRepository.setKeyguardOccluded(false)
+            keyguardRepository.setIsEncryptedOrLockdown(false)
+            setIsMainUser(true)
             tutorialRepository.setTutorialSettingState(
                 Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
             )
@@ -202,4 +209,10 @@
             underTest.onHidePopupAfterDismissCta()
             assertThat(isPopupOnDismissCtaShowing).isEqualTo(false)
         }
+
+    private suspend fun setIsMainUser(isMainUser: Boolean) {
+        whenever(user.isMain).thenReturn(isMainUser)
+        userRepository.setUserInfos(listOf(user))
+        userRepository.setSelectedUserInfo(user)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index 52305b1..05b5891 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -49,10 +49,10 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
-    private val trustRepository = kosmos.fakeTrustRepository
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val authenticationInteractor = kosmos.authenticationInteractor
+    private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository }
+    private val trustRepository by lazy { kosmos.fakeTrustRepository }
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private lateinit var underTest: DeviceEntryInteractor
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 562f96c..c143468 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -93,7 +93,7 @@
     @Rule
     public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
 
-    WindowManager.LayoutParams mWindowParams = new WindowManager.LayoutParams();
+    WindowManager.LayoutParams mWindowParams;
 
     @Mock
     IDreamOverlayCallback mDreamOverlayCallback;
@@ -184,6 +184,7 @@
         when(mDreamOverlayContainerViewController.getContainerView())
                 .thenReturn(mDreamOverlayContainerView);
 
+        mWindowParams = new WindowManager.LayoutParams();
         mService = new DreamOverlayService(
                 mContext,
                 mLifecycleOwner,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
new file mode 100644
index 0000000..ea766f8
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.slider
+
+import android.widget.SeekBar
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.fakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class SeekableSliderHapticPluginTest : SysuiTestCase() {
+
+    private val kosmos = Kosmos()
+
+    @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
+    @Mock private lateinit var vibratorHelper: VibratorHelper
+    private val seekBar = SeekBar(mContext)
+    private lateinit var plugin: SeekableSliderHapticPlugin
+
+    @Before
+    fun setup() {
+        whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0))
+    }
+
+    @Test
+    fun start_beginsTrackingSlider() = runOnStartedPlugin { assertThat(plugin.isTracking).isTrue() }
+
+    @Test
+    fun stop_stopsTrackingSlider() = runOnStartedPlugin {
+        // WHEN called to stop
+        plugin.stop()
+
+        // THEN stops tracking
+        assertThat(plugin.isTracking).isFalse()
+    }
+
+    @Test
+    fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin {
+        // WHEN the plugin is restarted
+        plugin.stop()
+        plugin.start()
+
+        // THEN the tracking begins again
+        assertThat(plugin.isTracking).isTrue()
+    }
+
+    @Test
+    fun onKeyDown_startsWaiting() = runOnStartedPlugin {
+        // WHEN a keyDown event is recorded
+        plugin.onKeyDown()
+
+        // THEN the timer starts waiting
+        assertThat(plugin.isKeyUpTimerWaiting).isTrue()
+    }
+
+    @Test
+    fun keyUpWaitComplete_triggersOnArrowUp() = runOnStartedPlugin {
+        // GIVEN an onKeyDown that starts the wait and a program progress change that advances the
+        // slider state to ARROW_HANDLE_MOVED_ONCE
+        plugin.onKeyDown()
+        plugin.onProgressChanged(seekBar, 50, false)
+        testScheduler.runCurrent()
+        assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+
+        // WHEN the key-up wait completes after the timeout plus a small buffer
+        advanceTimeBy(KEY_UP_TIMEOUT + 10L)
+
+        // THEN the onArrowUp event is delivered causing the slider tracker to move to IDLE
+        assertThat(plugin.trackerState).isEqualTo(SliderState.IDLE)
+        assertThat(plugin.isKeyUpTimerWaiting).isFalse()
+    }
+
+    @Test
+    fun onKeyDown_whileWaiting_restartsWait() = runOnStartedPlugin {
+        // GIVEN an onKeyDown that starts the wait and a program progress change that advances the
+        // slider state to ARROW_HANDLE_MOVED_ONCE
+        plugin.onKeyDown()
+        plugin.onProgressChanged(seekBar, 50, false)
+        testScheduler.runCurrent()
+        assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+
+        // WHEN half the timeout period has elapsed and a new keyDown event occurs
+        advanceTimeBy(KEY_UP_TIMEOUT / 2)
+        plugin.onKeyDown()
+
+        // AFTER advancing by a period of time that should have complete the original wait
+        advanceTimeBy(KEY_UP_TIMEOUT / 2 + 10L)
+
+        // THEN the timer is still waiting and the slider tracker remains on ARROW_HANDLE_MOVED_ONCE
+        assertThat(plugin.isKeyUpTimerWaiting).isTrue()
+        assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+    }
+
+    private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) =
+        with(kosmos) {
+            testScope.runTest {
+                createPlugin(this, UnconfinedTestDispatcher(testScheduler))
+                // GIVEN that the plugin is started
+                plugin.start()
+
+                // THEN run the test
+                test()
+            }
+        }
+
+    private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) {
+        plugin =
+            SeekableSliderHapticPlugin(
+                vibratorHelper,
+                kosmos.fakeSystemClock,
+                dispatcher,
+                scope,
+            )
+    }
+
+    companion object {
+        private const val KEY_UP_TIMEOUT = 100L
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 6f62afc..dc8b97a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -139,6 +139,18 @@
         }
 
     @Test
+    fun topClippingBounds() =
+        testScope.runTest {
+            assertThat(underTest.topClippingBounds.value).isNull()
+
+            underTest.topClippingBounds.value = 50
+            assertThat(underTest.topClippingBounds.value).isEqualTo(50)
+
+            underTest.topClippingBounds.value = 500
+            assertThat(underTest.topClippingBounds.value).isEqualTo(500)
+        }
+
+    @Test
     fun clockPosition() =
         testScope.runTest {
             assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 0))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index 2c3afb1..0b320a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -54,15 +54,17 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val repository = kosmos.fakeKeyguardRepository
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val commandQueue = FakeCommandQueue()
+    private val repository by lazy { kosmos.fakeKeyguardRepository }
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val commandQueue by lazy {
+        FakeCommandQueue()
+    }
     private val bouncerRepository = FakeKeyguardBouncerRepository()
     private val shadeRepository = FakeShadeRepository()
     private val transitionState: MutableStateFlow<ObservableTransitionState> =
         MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Gone))
 
-    private val underTest =
+    private val underTest by lazy {
         KeyguardInteractor(
             repository = repository,
             commandQueue = commandQueue,
@@ -73,6 +75,7 @@
             shadeRepository = shadeRepository,
             sceneInteractorProvider = { sceneInteractor },
         )
+    }
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index 9bccf4e2..ce43d4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -38,12 +38,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito
 import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import org.mockito.Spy
 
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -51,16 +51,19 @@
 class LightRevealScrimInteractorTest : SysuiTestCase() {
     private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository()
 
-    @Spy private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
+    private val fakeLightRevealScrimRepository by lazy {
+        Mockito.spy(FakeLightRevealScrimRepository())
+    }
 
     private val testScope = TestScope()
 
-    private val keyguardTransitionInteractor =
+    private val keyguardTransitionInteractor by lazy {
         KeyguardTransitionInteractorFactory.create(
                 scope = testScope.backgroundScope,
                 repository = fakeKeyguardTransitionRepository,
             )
             .keyguardTransitionInteractor
+    }
 
     private lateinit var underTest: LightRevealScrimInteractor
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
index 9daf186..199ffa6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
@@ -20,11 +20,14 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -36,6 +39,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -45,10 +49,18 @@
 class AlternateBouncerToAodTransitionViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
-    private val biometricSettingsRepository = kosmos.biometricSettingsRepository
-    private val underTest = kosmos.alternateBouncerToAodTransitionViewModel
+    private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+    private lateinit var underTest: AlternateBouncerToAodTransitionViewModel
+
+    @Before
+    fun setUp() {
+        keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+        fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
+        biometricSettingsRepository = kosmos.biometricSettingsRepository
+        underTest = kosmos.alternateBouncerToAodTransitionViewModel
+    }
 
     @Test
     fun deviceEntryParentViewAppear() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
index 3f7e0df..d443851 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
@@ -49,7 +49,9 @@
         }
     private val testScope = kosmos.testScope
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val underTest = kosmos.alternateBouncerToGoneTransitionViewModel
+    private val underTest by lazy {
+        kosmos.alternateBouncerToGoneTransitionViewModel
+    }
 
     @Test
     fun deviceEntryParentViewDisappear() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
index c7ab529..ff41ea2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
@@ -47,8 +47,8 @@
             }
         }
     private val testScope = kosmos.testScope
-    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val underTest = kosmos.alternateBouncerToPrimaryBouncerTransitionViewModel
+    private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+    private val underTest by lazy { kosmos.alternateBouncerToPrimaryBouncerTransitionViewModel }
 
     @Test
     fun deviceEntryParentViewDisappear() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index f1690daf..89e29cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -20,10 +20,12 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
@@ -41,6 +43,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -50,9 +53,16 @@
 class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
-    private val underTest = kosmos.dreamingToLockscreenTransitionViewModel
+    private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var underTest: DreamingToLockscreenTransitionViewModel
+
+    @Before
+    fun setUp() {
+        keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+        fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
+        underTest = kosmos.dreamingToLockscreenTransitionViewModel
+    }
 
     @Test
     fun shortcutsAlpha_bothShortcutsReceiveLastValue() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index f763a67..36b26a4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -29,6 +30,7 @@
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,8 +39,14 @@
 class GoneToDreamingTransitionViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val repository = kosmos.fakeKeyguardTransitionRepository
-    private val underTest = kosmos.goneToDreamingTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var underTest: GoneToDreamingTransitionViewModel
+
+    @Before
+    fun setUp() {
+        repository = kosmos.fakeKeyguardTransitionRepository
+        underTest = kosmos.goneToDreamingTransitionViewModel
+    }
 
     @Test
     fun runTest() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index fd2fd2f..6cc680b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -25,9 +25,14 @@
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalRepository
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -42,6 +47,7 @@
 import com.android.systemui.util.ui.value
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -54,11 +60,14 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val keyguardInteractor = kosmos.keyguardInteractor
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
+    private val communalRepository = kosmos.communalRepository
     private val screenOffAnimationController = kosmos.screenOffAnimationController
     private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
     private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor
     private val dozeParameters = kosmos.dozeParameters
-    private val underTest = kosmos.keyguardRootViewModel
+    private val underTest by lazy { kosmos.keyguardRootViewModel }
 
     @Before
     fun setUp() {
@@ -205,7 +214,38 @@
         }
 
     @Test
-    fun alpha_glanceableHubOpen_isZero() =
+    fun topClippingBounds() =
+        testScope.runTest {
+            val topClippingBounds by collectLastValue(underTest.topClippingBounds)
+            assertThat(topClippingBounds).isNull()
+
+            keyguardRepository.topClippingBounds.value = 50
+            assertThat(topClippingBounds).isEqualTo(50)
+
+            keyguardRepository.topClippingBounds.value = 1000
+            assertThat(topClippingBounds).isEqualTo(1000)
+        }
+
+    @Test
+    fun alpha_idleOnHub_isZero() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.alpha)
+
+            // Hub transition state is idle with hub open.
+            communalRepository.setTransitionState(
+                flowOf(ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal))
+            )
+            runCurrent()
+
+            // Set keyguard alpha to 1.0f.
+            keyguardInteractor.setAlpha(1.0f)
+
+            // Alpha property remains 0 regardless.
+            assertThat(alpha).isEqualTo(0f)
+        }
+
+    @Test
+    fun alpha_transitionToHub_isZero() =
         testScope.runTest {
             val alpha by collectLastValue(underTest.alpha)
 
@@ -219,7 +259,7 @@
         }
 
     @Test
-    fun alpha_glanceableHubClosed_isOne() =
+    fun alpha_transitionFromHubToLockscreen_isOne() =
         testScope.runTest {
             val alpha by collectLastValue(underTest.alpha)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index aa15d0b..4595fbf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -47,9 +47,11 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
 
-    private val underTest = createLockscreenSceneViewModel()
+    private val underTest by lazy {
+        createLockscreenSceneViewModel()
+    }
 
     @Test
     fun upTransitionSceneKey_canSwipeToUnlock_gone() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 74025fd..8f04ec38 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -27,18 +27,22 @@
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.testKosmos
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -51,10 +55,18 @@
             fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
         }
     private val testScope = kosmos.testScope
-    private val repository = kosmos.fakeKeyguardTransitionRepository
-    private val shadeRepository = kosmos.shadeRepository
-    private val keyguardRepository = kosmos.fakeKeyguardRepository
-    private val underTest = kosmos.lockscreenToDreamingTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var shadeRepository: ShadeRepository
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var underTest: LockscreenToDreamingTransitionViewModel
+
+    @Before
+    fun setUp() {
+        repository = kosmos.fakeKeyguardTransitionRepository
+        shadeRepository = kosmos.shadeRepository
+        keyguardRepository = kosmos.fakeKeyguardRepository
+        underTest = kosmos.lockscreenToDreamingTransitionViewModel
+    }
 
     @Test
     fun lockscreenFadeOut() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 6fcb0c1..b120f87 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -22,12 +22,15 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -35,12 +38,14 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.testKosmos
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -52,11 +57,20 @@
             fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
         }
     private val testScope = kosmos.testScope
-    private val repository = kosmos.fakeKeyguardTransitionRepository
-    private val shadeRepository = kosmos.shadeRepository
-    private val keyguardRepository = kosmos.fakeKeyguardRepository
-    private val configurationRepository = kosmos.fakeConfigurationRepository
-    private val underTest = kosmos.lockscreenToOccludedTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var shadeRepository: ShadeRepository
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var configurationRepository: FakeConfigurationRepository
+    private lateinit var underTest: LockscreenToOccludedTransitionViewModel
+
+    @Before
+    fun setUp() {
+        repository = kosmos.fakeKeyguardTransitionRepository
+        shadeRepository = kosmos.shadeRepository
+        keyguardRepository = kosmos.fakeKeyguardRepository
+        configurationRepository = kosmos.fakeConfigurationRepository
+        underTest = kosmos.lockscreenToOccludedTransitionViewModel
+    }
 
     @Test
     fun lockscreenFadeOut() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index 639114c..dddf648 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -49,7 +49,9 @@
     val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
     val configurationRepository = kosmos.fakeConfigurationRepository
-    val underTest = kosmos.occludedToLockscreenTransitionViewModel
+    val underTest by lazy {
+        kosmos.occludedToLockscreenTransitionViewModel
+    }
 
     @Test
     fun lockscreenFadeIn() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 30b87bb..30ac344 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -54,7 +54,9 @@
     val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     val primaryBouncerInteractor = kosmos.primaryBouncerInteractor
     val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
-    val underTest = kosmos.primaryBouncerToGoneTransitionViewModel
+    val underTest by lazy {
+        kosmos.primaryBouncerToGoneTransitionViewModel
+    }
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
index 96d5774..4207a9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
@@ -54,7 +54,9 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class WorkProfileAutoAddedAfterRestoreTest : SysuiTestCase() {
 
-    private val kosmos = Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) }
+    private val kosmos by lazy {
+        Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) }
+    }
     // Getter here so it can change when there is a managed profile.
     private val workTileAvailable: Boolean
         get() = hasManagedProfile()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt
index 8ee6d20..d05e98f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt
@@ -36,8 +36,9 @@
 class ColorCorrectionTileMapperTest : SysuiTestCase() {
     private val kosmos = Kosmos()
     private val colorCorrectionTileConfig = kosmos.qsColorCorrectionTileConfig
-    private val subtitleArray =
+    private val subtitleArray by lazy {
         context.resources.getStringArray(R.array.tile_states_color_correction)
+    }
     // Using lazy (versus =) to make sure we override the right context -- see b/311612168
     private val mapper by lazy {
         ColorCorrectionTileMapper(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
index a84b9fa..da60c18 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
@@ -308,21 +308,27 @@
         val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
 
         val TEST_USER_1 = UserHandle.of(1)!!
-        val TEST_TILE_1 =
+        val TEST_TILE_1 by lazy {
             Tile().apply {
                 label = "test_tile_1"
                 icon = Icon.createWithContentUri("file://test_1")
             }
+        }
         val TEST_TILE_KEY_1 = TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier)
-        val TEST_DEFAULTS_1 = CustomTileDefaults.Result(TEST_TILE_1.icon, TEST_TILE_1.label)
+        val TEST_DEFAULTS_1 by lazy {
+            CustomTileDefaults.Result(TEST_TILE_1.icon, TEST_TILE_1.label)
+        }
 
         val TEST_USER_2 = UserHandle.of(2)!!
-        val TEST_TILE_2 =
+        val TEST_TILE_2 by lazy {
             Tile().apply {
                 label = "test_tile_2"
                 icon = Icon.createWithContentUri("file://test_2")
             }
+        }
         val TEST_TILE_KEY_2 = TileServiceKey(TEST_COMPONENT, TEST_USER_2.identifier)
-        val TEST_DEFAULTS_2 = CustomTileDefaults.Result(TEST_TILE_2.icon, TEST_TILE_2.label)
+        val TEST_DEFAULTS_2 by lazy {
+            CustomTileDefaults.Result(TEST_TILE_2.icon, TEST_TILE_2.label)
+        }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
index 20653ca..995d6ac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
@@ -180,11 +180,14 @@
 
         val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
         val TEST_USER = UserHandle.of(1)!!
-        val TEST_TILE =
+        val TEST_TILE by lazy {
             Tile().apply {
                 label = "test_tile_1"
                 icon = Icon.createWithContentUri("file://test_1")
             }
-        val TEST_DEFAULTS = CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label)
+        }
+        val TEST_DEFAULTS by lazy {
+            CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label)
+        }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt
index f3c3579..ccd7ed9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt
@@ -39,7 +39,9 @@
     private val colorInversionTileConfig = kosmos.qsColorInversionTileConfig
     private val subtitleArrayId =
         SubtitleArrayMapping.getSubtitleId(colorInversionTileConfig.tileSpec.spec)
-    private val subtitleArray = context.resources.getStringArray(subtitleArrayId)
+    private val subtitleArray by lazy {
+        context.resources.getStringArray(subtitleArrayId)
+    }
     // Using lazy (versus =) to make sure we override the right context -- see b/311612168
     private val mapper by lazy {
         ColorInversionTileMapper(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt
index 7497ebd..46e1609 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt
@@ -53,8 +53,9 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class UiModeNightTileDataInteractorTest : SysuiTestCase() {
-    private val configurationController: ConfigurationController =
+    private val configurationController: ConfigurationController by lazy {
         ConfigurationControllerImpl(context)
+    }
     private val batteryController = FakeBatteryController(LeakCheck())
     private val locationController = FakeLocationController(LeakCheck())
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index d9b1ea1..cae20d0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -16,12 +16,16 @@
 
 package com.android.systemui.qs.ui.adapter
 
+import android.content.res.Configuration
 import android.os.Bundle
+import android.view.Surface
 import android.view.View
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.qs.QSImpl
 import com.android.systemui.qs.dagger.QSComponent
@@ -34,6 +38,7 @@
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import java.util.Locale
 import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
@@ -81,11 +86,17 @@
                     .also { components.add(it) }
             }
         }
+    private val configuration = Configuration(context.resources.configuration)
+
+    private val fakeConfigurationRepository =
+        FakeConfigurationRepository().apply { onConfigurationChange(configuration) }
+    private val configurationInteractor = ConfigurationInteractor(fakeConfigurationRepository)
 
     private val mockAsyncLayoutInflater =
         mock<AsyncLayoutInflater>() {
             whenever(inflate(anyInt(), nullable(), any())).then { invocation ->
                 val mockView = mock<View>()
+                whenever(mockView.context).thenReturn(context)
                 invocation
                     .getArgument<AsyncLayoutInflater.OnInflateFinishedListener>(2)
                     .onInflateFinished(
@@ -102,6 +113,7 @@
             qsImplProvider,
             testDispatcher,
             testScope.backgroundScope,
+            configurationInteractor,
             { mockAsyncLayoutInflater },
         )
 
@@ -297,6 +309,9 @@
     @Test
     fun reinflation_previousStateDestroyed() =
         testScope.runTest {
+            // Run all flows... In particular, initial configuration propagation that could cause
+            // QSImpl to re-inflate.
+            runCurrent()
             val qsImpl by collectLastValue(underTest.qsImpl)
 
             underTest.inflate(context)
@@ -322,4 +337,81 @@
                     bundleArgCaptor.value,
                 )
         }
+
+    @Test
+    fun changeInLocale_reinflation() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            val oldQsImpl = qsImpl!!
+
+            val newLocale =
+                if (configuration.locales[0] == Locale("en-US")) {
+                    Locale("es-UY")
+                } else {
+                    Locale("en-US")
+                }
+            configuration.setLocale(newLocale)
+            fakeConfigurationRepository.onConfigurationChange(configuration)
+            runCurrent()
+
+            assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+        }
+
+    @Test
+    fun changeInFontSize_reinflation() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            val oldQsImpl = qsImpl!!
+
+            configuration.fontScale *= 2
+            fakeConfigurationRepository.onConfigurationChange(configuration)
+            runCurrent()
+
+            assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+        }
+
+    @Test
+    fun changeInAssetPath_reinflation() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            val oldQsImpl = qsImpl!!
+
+            configuration.assetsSeq += 1
+            fakeConfigurationRepository.onConfigurationChange(configuration)
+            runCurrent()
+
+            assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+        }
+
+    @Test
+    fun otherChangesInConfiguration_noReinflation_configurationChangeDispatched() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            val oldQsImpl = qsImpl!!
+            configuration.densityDpi *= 2
+            configuration.windowConfiguration.maxBounds.scale(2f)
+            configuration.windowConfiguration.rotation = Surface.ROTATION_270
+            fakeConfigurationRepository.onConfigurationChange(configuration)
+            runCurrent()
+
+            assertThat(oldQsImpl).isSameInstanceAs(qsImpl!!)
+            verify(qsImpl!!).onConfigurationChanged(configuration)
+            verify(qsImpl!!.view).dispatchConfigurationChanged(configuration)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index be523b8..42200a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -23,6 +23,8 @@
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Direction
@@ -39,12 +41,16 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -52,10 +58,16 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
     private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
     private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() }
+    private val footerActionsViewModel = mock<FooterActionsViewModel>()
+    private val footerActionsViewModelFactory =
+        mock<FooterActionsViewModel.Factory> {
+            whenever(create(any())).thenReturn(footerActionsViewModel)
+        }
+    private val footerActionsController = mock<FooterActionsController>()
 
     private var mobileIconsViewModel: MobileIconsViewModel =
         MobileIconsViewModel(
@@ -94,6 +106,8 @@
                 shadeHeaderViewModel = shadeHeaderViewModel,
                 qsSceneAdapter = qsFlexiglassAdapter,
                 notifications = kosmos.notificationsPlaceholderViewModel,
+                footerActionsViewModelFactory,
+                footerActionsController,
             )
     }
 
@@ -125,4 +139,12 @@
                     )
                 )
         }
+
+    @Test
+    fun gettingViewModelInitializesControllerOnlyOnce() {
+        underTest.getFooterActionsViewModel(mock())
+        underTest.getFooterActionsViewModel(mock())
+
+        verify(footerActionsController, times(1)).init()
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 1cd764e..1e5ebd0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -123,30 +123,32 @@
 
     private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true }
     private val testScope = kosmos.testScope
-    private val sceneContainerConfig = kosmos.sceneContainerConfig
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val authenticationInteractor = kosmos.authenticationInteractor
-    private val deviceEntryInteractor = kosmos.deviceEntryInteractor
-    private val communalInteractor = kosmos.communalInteractor
+    private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig }
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
+    private val communalInteractor by lazy { kosmos.communalInteractor }
 
-    private val transitionState =
+    private val transitionState by lazy {
         MutableStateFlow<ObservableTransitionState>(
             ObservableTransitionState.Idle(sceneContainerConfig.initialSceneKey)
         )
-    private val sceneContainerViewModel =
+    }
+    private val sceneContainerViewModel by lazy {
         SceneContainerViewModel(
                 sceneInteractor = sceneInteractor,
                 falsingInteractor = kosmos.falsingInteractor,
             )
             .apply { setTransitionState(transitionState) }
+    }
 
-    private val bouncerInteractor = kosmos.bouncerInteractor
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
 
     private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
     private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor
     private lateinit var bouncerViewModel: BouncerViewModel
 
-    private val lockscreenSceneViewModel =
+    private val lockscreenSceneViewModel by lazy {
         LockscreenSceneViewModel(
             applicationScope = testScope.backgroundScope,
             deviceEntryInteractor = deviceEntryInteractor,
@@ -157,6 +159,7 @@
                 ),
             notifications = kosmos.notificationsPlaceholderViewModel,
         )
+    }
 
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
 
@@ -179,8 +182,8 @@
     private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
     private lateinit var shadeSceneViewModel: ShadeSceneViewModel
 
-    private val keyguardInteractor = kosmos.keyguardInteractor
-    private val powerInteractor = kosmos.powerInteractor
+    private val keyguardInteractor by lazy { kosmos.keyguardInteractor }
+    private val powerInteractor by lazy { kosmos.powerInteractor }
 
     private var bouncerSceneJob: Job? = null
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index fc0df12..16cb623 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -79,13 +79,13 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val sceneContainerFlags = kosmos.fakeSceneContainerFlags
-    private val authenticationInteractor = kosmos.authenticationInteractor
-    private val bouncerInteractor = kosmos.bouncerInteractor
-    private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
-    private val deviceEntryInteractor = kosmos.deviceEntryInteractor
-    private val keyguardInteractor = kosmos.keyguardInteractor
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val sceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags }
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository }
+    private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
+    private val keyguardInteractor by lazy { kosmos.keyguardInteractor }
     private val sysUiState: SysUiState = mock()
     private val falsingCollector: FalsingCollector = mock()
     private val powerInteractor = PowerInteractorFactory.create().powerInteractor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index ede453d8..a16ce43 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -41,7 +41,7 @@
 class SceneContainerViewModelTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
-    private val interactor = kosmos.sceneInteractor
+    private val interactor by lazy { kosmos.sceneInteractor }
     private lateinit var underTest: SceneContainerViewModel
 
     @Before
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 5174502..e9a2a3b 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
@@ -35,7 +35,7 @@
 class ShadeHeaderViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
 
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
     private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 251daff..5ef095f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -60,8 +60,8 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val deviceEntryInteractor = kosmos.deviceEntryInteractor
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
 
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
     private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt
index ef2046d..ad4b98b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt
@@ -69,7 +69,9 @@
     private lateinit var controller: CommunalSmartspaceController
 
     // TODO(b/272811280): Remove usage of real view
-    private val fakeParent = FrameLayout(context)
+    private val fakeParent by lazy {
+        FrameLayout(context)
+    }
 
     /**
      * A class which implements SmartspaceView and extends View. This is mocked to provide the right
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
index e093859..3a38631 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
@@ -73,8 +73,9 @@
     @Mock
     private lateinit var weatherViewComponent: SmartspaceViewComponent
 
-    @Spy
-    private var weatherSmartspaceView: SmartspaceView = TestView(context)
+    private val weatherSmartspaceView: SmartspaceView by lazy {
+        Mockito.spy(TestView(context))
+    }
 
     @Mock
     private lateinit var targetFilter: SmartspaceTargetFilter
@@ -88,8 +89,9 @@
     @Mock
     private lateinit var precondition: SmartspacePrecondition
 
-    @Spy
-    private var smartspaceView: SmartspaceView = TestView(context)
+    private val smartspaceView: SmartspaceView by lazy {
+        Mockito.spy(TestView(context))
+    }
 
     @Mock
     private lateinit var listener: BcSmartspaceDataPlugin.SmartspaceTargetListener
@@ -100,7 +102,9 @@
     private lateinit var controller: DreamSmartspaceController
 
     // TODO(b/272811280): Remove usage of real view
-    private val fakeParent = FrameLayout(context)
+    private val fakeParent by lazy {
+        FrameLayout(context)
+    }
 
     /**
      * A class which implements SmartspaceView and extends View. This is mocked to provide the right
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 607996d..6a2e317 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -56,9 +56,9 @@
             }
         }
     private val testScope = kosmos.testScope
-    private val placeholderViewModel = kosmos.notificationsPlaceholderViewModel
-    private val appearanceViewModel = kosmos.notificationStackAppearanceViewModel
-    private val sceneInteractor = kosmos.sceneInteractor
+    private val placeholderViewModel by lazy { kosmos.notificationsPlaceholderViewModel }
+    private val appearanceViewModel by lazy { kosmos.notificationStackAppearanceViewModel }
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
 
     @Test
     fun updateBounds() =
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
index 3d9645a..b1736b1 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
@@ -227,5 +227,10 @@
         void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch);
         // requires version 2
         void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs);
+
+        /**
+         * Callback function for when the volume changed due to a physical key press.
+         */
+        void onVolumeChangedFromKey();
     }
 }
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml
new file mode 100644
index 0000000..045c19e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml
new file mode 100644
index 0000000..5e012ab
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillColor="#fff"/>
+</vector>
+
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml
new file mode 100644
index 0000000..d8a9a70
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
new file mode 100644
index 0000000..dec9930
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    >
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/bindable_status_bar_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml
new file mode 100644
index 0000000..ee4d05c
--- /dev/null
+++ b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<!-- Base layout that provides a single bindable icon_view id image view -->
+<com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    >
+
+    <ImageView
+        android:id="@+id/icon_view"
+        android:layout_height="@dimen/status_bar_bindable_icon_size"
+        android:layout_width="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:padding="@dimen/status_bar_bindable_icon_padding"
+        android:scaleType="fitCenter"
+        />
+
+</com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView>
diff --git a/packages/SystemUI/res/layout/qs_customize_panel_content.xml b/packages/SystemUI/res/layout/qs_customize_panel_content.xml
index 3be9993..156c983 100644
--- a/packages/SystemUI/res/layout/qs_customize_panel_content.xml
+++ b/packages/SystemUI/res/layout/qs_customize_panel_content.xml
@@ -37,6 +37,7 @@
             android:layout_height="wrap_content"
             android:background="@drawable/qs_customizer_toolbar"
             android:navigationContentDescription="@*android:string/action_bar_up_description"
+            android:titleTextAppearance="@*android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title"
             style="@style/QSCustomizeToolbar"
             />
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ee2a1ce..bc221b8 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -151,6 +151,8 @@
     <dimen name="status_bar_icon_size_sp">@*android:dimen/status_bar_icon_size_sp</dimen>
     <!-- Original dp height of notification icons in the status bar -->
     <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen>
+    <dimen name="status_bar_bindable_icon_size">20sp</dimen>
+    <dimen name="status_bar_bindable_icon_padding">2sp</dimen>
 
     <!-- Default horizontal drawable padding for status bar icons. -->
     <dimen name="status_bar_horizontal_padding">2.5sp</dimen>
@@ -1726,6 +1728,10 @@
     <dimen name="communal_grid_height">630dp</dimen>
     <!-- Number of columns for each communal card -->
     <integer name="communal_grid_columns_per_card">6</integer>
+    <!-- Width of area on right edge of screen in which swipes will open the communal hub -->
+    <dimen name="communal_right_edge_swipe_region_width">16dp</dimen>
+    <!-- Height of area at top of communal hub where swipes should open the notification shade -->
+    <dimen name="communal_top_edge_swipe_region_height">32dp</dimen>
 
     <dimen name="drag_and_drop_icon_size">70dp</dimen>
 
@@ -1930,5 +1936,9 @@
     <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>
 
     <!-- UDFPS view attributes -->
-    <dimen name="udfps_icon_size">6mm</dimen>
+    <!-- UDFPS icon size in microns/um -->
+    <dimen name="udfps_icon_size" format="float">6000</dimen>
+    <!-- Microns/ums (1000 um = 1mm) per pixel for the given device. If unspecified, UI that
+         relies on this value will not be sized correctly. -->
+    <item name="pixel_pitch" format="float" type="dimen">-1</item>
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 7088829..d191a3c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -20,7 +20,6 @@
 import android.graphics.Region;
 import android.os.Bundle;
 import android.view.MotionEvent;
-import android.view.SurfaceControl;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
 // Next ID: 29
@@ -99,11 +98,6 @@
     void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
 
     /**
-     * Sent when the surface for navigation bar is created or changed
-     */
-    void onNavigationBarSurface(in SurfaceControl surface) = 26;
-
-    /**
      * Sent when the task bar stash state is toggled.
      */
     void onTaskbarToggled() = 27;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 033f93b..ad30317 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -477,9 +477,13 @@
     public void dump(PrintWriter pw, String[] args) {
         pw.println("KeyguardClockSwitch:");
         pw.println("  mSmallClockFrame = " + mSmallClockFrame);
-        pw.println("  mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha());
+        if (mSmallClockFrame != null) {
+            pw.println("  mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha());
+        }
         pw.println("  mLargeClockFrame = " + mLargeClockFrame);
-        pw.println("  mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha());
+        if (mLargeClockFrame != null) {
+            pw.println("  mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha());
+        }
         pw.println("  mStatusArea = " + mStatusArea);
         pw.println("  mDisplayedClockSize = " + mDisplayedClockSize);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 6869bba..97999cc 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -240,7 +240,9 @@
         mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> {
             if (Flags.floatingMenuDragToHide()) {
                 dismissNotification();
-                undo();
+                if (newTargetFeatures.size() > 0) {
+                    undo();
+                }
             } else {
                 if (newTargetFeatures.size() < 1) {
                     return;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
index 4fc1b58..a77cc1f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.biometrics.domain.interactor
 
 import android.content.Context
+import android.util.Log
 import android.view.MotionEvent
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
@@ -42,12 +43,23 @@
 class UdfpsOverlayInteractor
 @Inject
 constructor(
-    @Application context: Context,
+    @Application private val context: Context,
     private val authController: AuthController,
     private val selectedUserInteractor: SelectedUserInteractor,
     @Application scope: CoroutineScope
 ) {
-    private val iconSize: Int = context.resources.getDimensionPixelSize(R.dimen.udfps_icon_size)
+    private fun calculateIconSize(): Int {
+        val pixelPitch = context.resources.getFloat(R.dimen.pixel_pitch)
+        if (pixelPitch <= 0) {
+            Log.e(
+                "UdfpsOverlayInteractor",
+                "invalid pixelPitch: $pixelPitch. Pixel pitch must be updated per device."
+            )
+        }
+        return (context.resources.getFloat(R.dimen.udfps_icon_size) / pixelPitch).toInt()
+    }
+
+    private var iconSize: Int = calculateIconSize()
 
     /** Whether a touch is within the under-display fingerprint sensor area */
     fun isTouchWithinUdfpsArea(ev: MotionEvent): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
index 1353985..5f6ff82 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
@@ -72,6 +72,9 @@
     val onAnyConfigurationChange: Flow<Unit> =
         repository.onAnyConfigurationChange.onStart { emit(Unit) }
 
+    /** Emits the new configuration on any configuration change */
+    val configurationValues: Flow<Configuration> = repository.configurationValues
+
     /** Emits the current resolution scaling factor */
     val scaleForResolution: Flow<Float> = repository.scaleForResolution
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 92d01db..44b0383 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.smartspace.data.repository.SmartspaceRepository
+import com.android.systemui.user.data.repository.UserRepository
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -62,6 +63,7 @@
     private val communalPrefsRepository: CommunalPrefsRepository,
     mediaRepository: CommunalMediaRepository,
     smartspaceRepository: SmartspaceRepository,
+    userRepository: UserRepository,
     keyguardInteractor: KeyguardInteractor,
     private val appWidgetHost: CommunalAppWidgetHost,
     private val editWidgetsActivityStarter: EditWidgetsActivityStarter
@@ -75,7 +77,14 @@
     val isCommunalAvailable: StateFlow<Boolean> =
         flowOf(isCommunalEnabled)
             .flatMapLatest { enabled ->
-                if (enabled) keyguardInteractor.isEncryptedOrLockdown.map { !it } else flowOf(false)
+                if (enabled)
+                    combine(
+                        keyguardInteractor.isEncryptedOrLockdown,
+                        userRepository.selectedUserInfo,
+                    ) { isEncryptedOrLockdown, selectedUserInfo ->
+                        !isEncryptedOrLockdown && selectedUserInfo.isMain
+                    }
+                else flowOf(false)
             }
             .distinctUntilChanged()
             .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) }
@@ -263,6 +272,12 @@
 
     companion object {
         /**
+         * The user activity timeout which should be used when the communal hub is opened. A value
+         * of -1 means that the user's chosen screen timeout will be used instead.
+         */
+        const val AWAKE_INTERVAL_MS = -1
+
+        /**
          * Calculates the content size dynamically based on the total number of contents of that
          * type.
          *
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
index 5ca89f2..4e5be9b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
@@ -45,14 +45,17 @@
     private val communalTutorialRepository: CommunalTutorialRepository,
     keyguardInteractor: KeyguardInteractor,
     private val communalRepository: CommunalRepository,
+    communalInteractor: CommunalInteractor,
 ) {
     /** An observable for whether the tutorial is available. */
     val isTutorialAvailable: Flow<Boolean> =
         combine(
+                communalInteractor.isCommunalAvailable,
                 keyguardInteractor.isKeyguardVisible,
                 communalTutorialRepository.tutorialSettingState,
-            ) { isKeyguardVisible, tutorialSettingState ->
-                isKeyguardVisible &&
+            ) { isCommunalAvailable, isKeyguardVisible, tutorialSettingState ->
+                isCommunalAvailable &&
+                    isKeyguardVisible &&
                     tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
             }
             .distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 2bb9d0e..a909383 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -74,6 +74,7 @@
         // before the MediaHierarchyManager attempts to move the UMO to the hub.
         with(mediaHost) {
             expansion = MediaHostState.EXPANDED
+            expandedMatchesParentHeight = true
             showsOnlyActiveMedia = false
             falsingProtectionNeeded = false
             init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 641064b..2f9fd2e 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.compose
 
+import android.app.Dialog
 import android.content.Context
 import android.view.View
 import android.view.WindowInsets
@@ -26,11 +27,13 @@
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.scene.shared.model.Scene
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.StateFlow
 
@@ -86,6 +89,12 @@
         sceneByKey: Map<SceneKey, Scene>,
     ): View
 
+    /** Creates sticky key dialog presenting provided [viewModel] **/
+    fun createStickyKeysDialog(
+        dialogFactory: SystemUIDialogFactory,
+        viewModel: StickyKeysIndicatorViewModel
+    ): Dialog
+
     /** Create a [View] to represent [viewModel] on screen. */
     fun createCommunalView(
         context: Context,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
index db0c3c6..0fd6887 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
@@ -18,10 +18,7 @@
 
 import android.content.Context;
 import android.hardware.Sensor;
-import android.os.Handler;
 
-import com.android.systemui.res.R;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.doze.DozeAuthRemover;
 import com.android.systemui.doze.DozeBrightnessHostForwarder;
@@ -40,6 +37,7 @@
 import com.android.systemui.doze.DozeTriggers;
 import com.android.systemui.doze.DozeUi;
 import com.android.systemui.doze.DozeWallpaperState;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
@@ -75,9 +73,8 @@
 
     @Provides
     @DozeScope
-    static WakeLock providesDozeWakeLock(DelayedWakeLock.Builder delayedWakeLockBuilder,
-            @Main Handler handler) {
-        return delayedWakeLockBuilder.setHandler(handler).setTag("Doze").build();
+    static WakeLock providesDozeWakeLock(DelayedWakeLock.Factory delayedWakeLockFactory) {
+        return delayedWakeLockFactory.create("Doze");
     }
 
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 846736c..3f7c152 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -574,9 +574,6 @@
     @JvmField
     val ENABLE_NEW_PRIVACY_DIALOG = releasedFlag("enable_new_privacy_dialog")
 
-    // TODO(b/289573946): Tracking Bug
-    @JvmField val PRECOMPUTED_TEXT = releasedFlag("precomputed_text")
-
     // TODO(b/302087895): Tracking Bug
     @JvmField val CALL_LAYOUT_ASYNC_SET_DATA =
             unreleasedFlag("call_layout_async_set_data", teamfood = true)
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
new file mode 100644
index 0000000..58fb6a9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.slider
+
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.widget.SeekBar
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback.
+ *
+ * A [SeekableSliderEventProducer] is used as the producer of slider events, a
+ * [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback
+ * depending on the state, and a [SeekableSliderTracker] is used as the state machine handler that
+ * tracks and manipulates the slider state.
+ */
+class SeekableSliderHapticPlugin
+@JvmOverloads
+constructor(
+    vibratorHelper: VibratorHelper,
+    systemClock: SystemClock,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Application private val applicationScope: CoroutineScope,
+    sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
+    sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
+) {
+
+    private val velocityTracker = VelocityTracker.obtain()
+
+    private val sliderEventProducer = SeekableSliderEventProducer()
+
+    private val sliderHapticFeedbackProvider =
+        SliderHapticFeedbackProvider(
+            vibratorHelper,
+            velocityTracker,
+            sliderHapticFeedbackConfig,
+            systemClock,
+        )
+
+    private val sliderTracker =
+        SeekableSliderTracker(
+            sliderHapticFeedbackProvider,
+            sliderEventProducer,
+            mainDispatcher,
+            sliderTrackerConfig,
+        )
+
+    val isTracking: Boolean
+        get() = sliderTracker.isTracking
+
+    val trackerState: SliderState
+        get() = sliderTracker.currentState
+
+    /**
+     * A waiting [Job] for a timer that estimates the key-up event when a key-down event is
+     * received.
+     *
+     * This is useful for the cases where the slider is being operated by an external key, but the
+     * release of the key is not easily accessible (e.g., the volume keys)
+     */
+    private var keyUpJob: Job? = null
+
+    @VisibleForTesting
+    val isKeyUpTimerWaiting: Boolean
+        get() = keyUpJob != null && keyUpJob?.isActive == true
+
+    /**
+     * Start the plugin.
+     *
+     * This starts the tracking of slider states, events and triggering of haptic feedback.
+     */
+    fun start() {
+        if (!isTracking) {
+            sliderTracker.startTracking()
+        }
+    }
+
+    /**
+     * Stop the plugin
+     *
+     * This stops the tracking of slider states, events and triggers of haptic feedback.
+     */
+    fun stop() = sliderTracker.stopTracking()
+
+    /** React to a touch event */
+    fun onTouchEvent(event: MotionEvent?) {
+        when (event?.actionMasked) {
+            MotionEvent.ACTION_UP,
+            MotionEvent.ACTION_CANCEL -> velocityTracker.clear()
+            MotionEvent.ACTION_DOWN,
+            MotionEvent.ACTION_MOVE -> velocityTracker.addMovement(event)
+        }
+    }
+
+    /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */
+    fun onStartTrackingTouch(seekBar: SeekBar) {
+        if (isTracking) {
+            sliderEventProducer.onStartTrackingTouch(seekBar)
+        }
+    }
+
+    /** onProgressChanged event from the slider's [android.widget.SeekBar] */
+    fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+        if (isTracking) {
+            sliderEventProducer.onProgressChanged(seekBar, progress, fromUser)
+        }
+    }
+
+    /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */
+    fun onStopTrackingTouch(seekBar: SeekBar) {
+        if (isTracking) {
+            sliderEventProducer.onStopTrackingTouch(seekBar)
+        }
+    }
+
+    /** onArrowUp event recorded */
+    fun onArrowUp() {
+        if (isTracking) {
+            sliderEventProducer.onArrowUp()
+        }
+    }
+
+    /**
+     * An external key was pressed (e.g., a volume key).
+     *
+     * This event is used to estimate the key-up event based on by running a timer as a waiting
+     * coroutine in the [keyUpTimerScope]. A key-up event in a slider corresponds to an onArrowUp
+     * event. Therefore, [onArrowUp] must be called after the timeout.
+     */
+    fun onKeyDown() {
+        if (!isTracking) return
+
+        if (isKeyUpTimerWaiting) {
+            // Cancel the ongoing wait
+            keyUpJob?.cancel()
+        }
+        keyUpJob =
+            applicationScope.launch {
+                delay(KEY_UP_TIMEOUT)
+                onArrowUp()
+            }
+    }
+
+    companion object {
+        const val KEY_UP_TIMEOUT = 100L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt
index 37034f6..4ed8120 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt
@@ -26,11 +26,20 @@
 private const val TAG = "stickyKeys"
 
 class StickyKeysLogger @Inject constructor(@KeyboardLog private val buffer: LogBuffer) {
-    fun logNewStickyKeysReceived(linkedHashMap: Map<ModifierKey, Locked>) {
+    fun logNewStickyKeysReceived(stickyKeys: Map<ModifierKey, Locked>) {
         buffer.log(
             TAG,
             LogLevel.VERBOSE,
-            { str1 = linkedHashMap.toString() },
+            { str1 = stickyKeys.toString() },
+            { "new sticky keys state received: $str1" }
+        )
+    }
+
+    fun logNewUiState(stickyKeys: Map<ModifierKey, Locked>) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { str1 = stickyKeys.toString() },
             { "new sticky keys state received: $str1" }
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
new file mode 100644
index 0000000..b68551b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.stickykeys.ui
+
+import android.app.Dialog
+import android.util.Log
+import android.view.Gravity
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.Window
+import android.view.WindowManager
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.stickykeys.StickyKeysLogger
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@SysUISingleton
+class StickyKeysIndicatorCoordinator
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val dialogFactory: SystemUIDialogFactory,
+    private val viewModel: StickyKeysIndicatorViewModel,
+    private val stickyKeysLogger: StickyKeysLogger,
+) {
+
+    private var dialog: Dialog? = null
+
+    fun startListening() {
+        // this check needs to be moved to PhysicalKeyboardCoreStartable
+        if (!ComposeFacade.isComposeAvailable()) {
+            Log.e("StickyKeysIndicatorCoordinator", "Compose is required for this UI")
+            return
+        }
+        applicationScope.launch {
+            viewModel.indicatorContent.collect { stickyKeys ->
+                stickyKeysLogger.logNewUiState(stickyKeys)
+                if (stickyKeys.isEmpty()) {
+                    dialog?.dismiss()
+                    dialog = null
+                } else if (dialog == null) {
+                    dialog = ComposeFacade.createStickyKeysDialog(dialogFactory, viewModel).apply {
+                        setCanceledOnTouchOutside(false)
+                        window?.setAttributes()
+                        show()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun Window.setAttributes() {
+        setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
+        addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
+        clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+        setGravity(Gravity.TOP or Gravity.END)
+        attributes = WindowManager.LayoutParams().apply {
+            copyFrom(attributes)
+            width = WRAP_CONTENT
+            title = "StickyKeysIndicator"
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index d012d24..1437194 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -132,6 +132,9 @@
      */
     val isDozing: StateFlow<Boolean>
 
+    /** Keyguard can be clipped at the top as the shade is dragged */
+    val topClippingBounds: MutableStateFlow<Int?>
+
     /**
      * Observable for whether the device is dreaming.
      *
@@ -326,6 +329,8 @@
     private val _clockShouldBeCentered = MutableStateFlow(true)
     override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered.asStateFlow()
 
+    override val topClippingBounds = MutableStateFlow<Int?>(null)
+
     override val isKeyguardShowing: Flow<Boolean> =
         conflatedCallbackFlow {
                 val callback =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 3292ea8..4df5d50 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -95,6 +95,7 @@
 
     companion object {
         const val TAG = "FromGlanceableHubTransitionInteractor"
-        val DEFAULT_DURATION = 500.milliseconds
+        val DEFAULT_DURATION = 400.milliseconds
+        val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 6170356..91747e0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -171,6 +171,14 @@
     /** Whether the keyguard is going away. */
     val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
 
+    /** Keyguard can be clipped at the top as the shade is dragged */
+    val topClippingBounds: Flow<Int?> =
+        combine(configurationInteractor.onAnyConfigurationChange, repository.topClippingBounds) {
+            _,
+            topClippingBounds ->
+            topClippingBounds
+        }
+
     /** Last point that [KeyguardRootView] view was tapped */
     val lastRootViewTapPosition: Flow<Point?> = repository.lastRootViewTapPosition.asStateFlow()
 
@@ -328,6 +336,10 @@
         repository.keyguardDoneAnimationsFinished()
     }
 
+    fun setTopClippingBounds(top: Int?) {
+        repository.topClippingBounds.value = top
+    }
+
     companion object {
         private const val TAG = "KeyguardInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 2aebd99..48092c6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -21,6 +21,7 @@
 import android.annotation.DrawableRes
 import android.annotation.SuppressLint
 import android.graphics.Point
+import android.graphics.Rect
 import android.view.HapticFeedbackConstants
 import android.view.View
 import android.view.View.OnLayoutChangeListener
@@ -158,6 +159,23 @@
                         }
 
                         launch {
+                            val clipBounds = Rect()
+                            viewModel.topClippingBounds.collect { clipTop ->
+                                if (clipTop == null) {
+                                    view.setClipBounds(null)
+                                } else {
+                                    clipBounds.apply {
+                                        top = clipTop
+                                        left = view.getLeft()
+                                        right = view.getRight()
+                                        bottom = view.getBottom()
+                                    }
+                                    view.setClipBounds(clipBounds)
+                                }
+                            }
+                        }
+
+                        launch {
                             viewModel.lockscreenStateAlpha.collect { alpha ->
                                 childViews[statusViewId]?.alpha = alpha
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
index bc51821..6aa2eca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
@@ -37,7 +37,7 @@
 ) {
     private val transitionAnimation =
         animationFlow.setup(
-            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
             from = KeyguardState.GLANCEABLE_HUB,
             to = KeyguardState.LOCKSCREEN,
         )
@@ -45,10 +45,20 @@
     // TODO(b/315205222): implement full animation spec instead of just a simple fade.
     val keyguardAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
-            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
             onStep = { it },
             onFinish = { 1f },
             onCancel = { 0f },
             name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha",
         )
+
+    // TODO(b/315205216): implement full animation spec instead of just a simple fade.
+    val notificationAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
+            onStep = { it },
+            onFinish = { 1f },
+            onCancel = { 0f },
+            name = "GLANCEABLE_HUB->LOCKSCREEN: notificationAlpha",
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 5d36da9..709e184 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -21,6 +21,7 @@
 import android.view.View.VISIBLE
 import com.android.systemui.Flags.newAodTransition
 import com.android.systemui.common.shared.model.NotificationContainerBounds
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -56,6 +57,7 @@
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val dozeParameters: DozeParameters,
     private val keyguardInteractor: KeyguardInteractor,
+    communalInteractor: CommunalInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
     aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
@@ -80,13 +82,31 @@
     val notificationBounds: StateFlow<NotificationContainerBounds> =
         keyguardInteractor.notificationContainerBounds
 
+    /**
+     * The keyguard root view can be clipped as the shade is pulled down, typically only for
+     * non-split shade cases.
+     */
+    val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds
+
     /** An observable for the alpha level for the entire keyguard root view. */
     val alpha: Flow<Float> =
-        merge(
-                aodAlphaViewModel.alpha,
-                lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
-                glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
-            )
+        combine(
+                communalInteractor.isIdleOnCommunal,
+                merge(
+                    aodAlphaViewModel.alpha,
+                    lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
+                    glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
+                )
+            ) { isIdleOnCommunal, alpha ->
+                if (isIdleOnCommunal) {
+                    // Keyguard should not show while the communal hub is fully visible. This check
+                    // is added since at the moment, closing the notification shade will cause the
+                    // keyguard alpha to be set back to 1.
+                    0f
+                } else {
+                    alpha
+                }
+            }
             .distinctUntilChanged()
 
     /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
index 3ea83ae..3afa49e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
@@ -51,4 +51,14 @@
             onCancel = { 1f },
             name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha",
         )
+
+    // TODO(b/315205216): implement full animation spec instead of just a simple fade.
+    val notificationAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            onStep = { 1f - it },
+            onFinish = { 0f },
+            onCancel = { 1f },
+            name = "LOCKSCREEN->GLANCEABLE_HUB: notificationAlpha",
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
index 631a0b8..437218f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
@@ -228,6 +228,14 @@
                 }
             }
 
+        override var expandedMatchesParentHeight: Boolean = false
+            set(value) {
+                if (value != field) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
         override var squishFraction: Float = 1.0f
             set(value) {
                 if (!value.equals(field)) {
@@ -282,6 +290,7 @@
         override fun copy(): MediaHostState {
             val mediaHostState = MediaHostStateHolder()
             mediaHostState.expansion = expansion
+            mediaHostState.expandedMatchesParentHeight = expandedMatchesParentHeight
             mediaHostState.squishFraction = squishFraction
             mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
             mediaHostState.measurementInput = measurementInput?.copy()
@@ -360,6 +369,12 @@
      */
     var expansion: Float
 
+    /**
+     * If true, the [EXPANDED] layout should stretch to match the height of its parent container,
+     * rather than having a fixed height.
+     */
+    var expandedMatchesParentHeight: Boolean
+
     /** Fraction of the height animation. */
     var squishFraction: Float
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index be93936..962764c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -20,6 +20,7 @@
 import android.content.res.Configuration
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT
 import com.android.app.tracing.traceSection
 import com.android.systemui.media.controls.models.GutsViewHolder
 import com.android.systemui.media.controls.models.player.MediaViewHolder
@@ -152,18 +153,11 @@
                         lastOrientation = newOrientation
                         // Update the height of media controls for the expanded layout. it is needed
                         // for large screen devices.
-                        val backgroundIds =
-                            if (type == TYPE.PLAYER) {
-                                MediaViewHolder.backgroundIds
-                            } else {
-                                setOf(RecommendationViewHolder.backgroundId)
-                            }
-                        backgroundIds.forEach { id ->
-                            expandedLayout.getConstraint(id).layout.mHeight =
-                                context.resources.getDimensionPixelSize(
-                                    R.dimen.qs_media_session_height_expanded
-                                )
-                        }
+                        setBackgroundHeights(
+                            context.resources.getDimensionPixelSize(
+                                R.dimen.qs_media_session_height_expanded
+                            )
+                        )
                     }
                     if (this@MediaViewController::configurationChangeListener.isInitialized) {
                         configurationChangeListener.invoke()
@@ -276,6 +270,17 @@
     private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
         if (expansion > 0) expandedLayout else collapsedLayout
 
+    /** Set the height of UMO background constraints. */
+    private fun setBackgroundHeights(height: Int) {
+        val backgroundIds =
+            if (type == TYPE.PLAYER) {
+                MediaViewHolder.backgroundIds
+            } else {
+                setOf(RecommendationViewHolder.backgroundId)
+            }
+        backgroundIds.forEach { id -> expandedLayout.getConstraint(id).layout.mHeight = height }
+    }
+
     /**
      * Set the views to be showing/hidden based on the [isGutsVisible] for a given
      * [TransitionViewState].
@@ -454,6 +459,18 @@
         }
         // Let's create a new measurement
         if (state.expansion == 0.0f || state.expansion == 1.0f) {
+            if (state.expansion == 1.0f) {
+                val height =
+                    if (state.expandedMatchesParentHeight) {
+                        MATCH_CONSTRAINT
+                    } else {
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.qs_media_session_height_expanded
+                        )
+                    }
+                setBackgroundHeights(height)
+            }
+
             result =
                 transitionLayout!!.calculateViewState(
                     state.measurementInput!!,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 0a72a2f..068e5fd 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -504,24 +504,6 @@
                 }
             };
 
-    private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback =
-            new SurfaceChangedCallback() {
-            @Override
-            public void surfaceCreated(Transaction t) {
-                notifyNavigationBarSurface();
-            }
-
-            @Override
-            public void surfaceDestroyed() {
-                notifyNavigationBarSurface();
-            }
-
-            @Override
-            public void surfaceReplaced(Transaction t) {
-                notifyNavigationBarSurface();
-            }
-    };
-
     private boolean mScreenPinningActive = false;
     private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
         @Override
@@ -787,8 +769,6 @@
 
         mView.getViewTreeObserver().addOnComputeInternalInsetsListener(
                 mOnComputeInternalInsetsListener);
-        mView.getViewRootImpl().addSurfaceChangedCallback(mSurfaceChangedCallback);
-        notifyNavigationBarSurface();
 
         mPipOptional.ifPresent(mView::addPipExclusionBoundsChangeListener);
         mBackAnimation.ifPresent(mView::registerBackAnimation);
@@ -860,13 +840,8 @@
         mHandler.removeCallbacks(mEnableLayoutTransitions);
         mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
         mPipOptional.ifPresent(mView::removePipExclusionBoundsChangeListener);
-        ViewRootImpl viewRoot = mView.getViewRootImpl();
-        if (viewRoot != null) {
-            viewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback);
-        }
         mFrame = null;
         mOrientationHandle = null;
-        notifyNavigationBarSurface();
     }
 
     // TODO: Remove this when we update nav bar recreation
@@ -1026,17 +1001,6 @@
         }
     }
 
-    private void notifyNavigationBarSurface() {
-        ViewRootImpl viewRoot = mView.getViewRootImpl();
-        SurfaceControl surface = mView.getParent() != null 
-                && viewRoot != null
-                && viewRoot.getSurfaceControl() != null
-                && viewRoot.getSurfaceControl().isValid()
-                        ? viewRoot.getSurfaceControl()
-                        : null;
-        mOverviewProxyService.onNavigationBarSurfaceChanged(surface);
-    }
-
     private int deltaRotation(int oldRotation, int newRotation) {
         int delta = newRotation - oldRotation;
         if (delta < 0) delta += 4;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index a3b9254..a2dfc01 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -27,8 +27,11 @@
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
+
 import com.android.systemui.Dumpable;
 import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.res.R;
@@ -53,6 +56,7 @@
     private QuickStatusBarHeader mHeader;
     private float mQsExpansion;
     private QSCustomizer mQSCustomizer;
+    private QSPanel mQSPanel;
     private NonInterceptingScrollView mQSPanelContainer;
 
     private int mHorizontalMargins;
@@ -72,6 +76,7 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view);
+        mQSPanel = findViewById(R.id.quick_settings_panel);
         mHeader = findViewById(R.id.header);
         mQSCustomizer = findViewById(R.id.qs_customize);
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
@@ -79,6 +84,13 @@
 
     void setSceneContainerEnabled(boolean enabled) {
         mSceneContainerEnabled = enabled;
+        if (enabled) {
+            mQSPanelContainer.removeAllViews();
+            removeView(mQSPanelContainer);
+            LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT);
+            addView(mQSPanel, 0, lp);
+        }
     }
 
     @Override
@@ -97,20 +109,26 @@
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the
         // bottom and footer are inside the screen.
-        MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
-
         int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec);
-        int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
-                - getPaddingBottom();
-        int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
-                + layoutParams.rightMargin;
-        final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding,
-                layoutParams.width);
-        mQSPanelContainer.measure(qsPanelWidthSpec,
-                MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
-        int width = mQSPanelContainer.getMeasuredWidth() + padding;
-        super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
+
+        if (!mSceneContainerEnabled) {
+            MarginLayoutParams layoutParams =
+                    (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
+            int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
+                    - getPaddingBottom();
+            int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
+                    + layoutParams.rightMargin;
+            final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding,
+                    layoutParams.width);
+            mQSPanelContainer.measure(qsPanelWidthSpec,
+                    MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
+            int width = mQSPanelContainer.getMeasuredWidth() + padding;
+            super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
+        } else {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+
         // QSCustomizer will always be the height of the screen, but do this after
         // other measuring to avoid changing the height of the QS.
         mQSCustomizer.measure(widthMeasureSpec,
@@ -130,12 +148,15 @@
     @Override
     protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
             int parentHeightMeasureSpec, int heightUsed) {
-        // Do not measure QSPanel again when doing super.onMeasure.
-        // This prevents the pages in PagedTileLayout to be remeasured with a different (incorrect)
-        // size to the one used for determining the number of rows and then the number of pages.
-        if (child != mQSPanelContainer) {
-            super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
-                    parentHeightMeasureSpec, heightUsed);
+        if (!mSceneContainerEnabled) {
+            // Do not measure QSPanel again when doing super.onMeasure.
+            // This prevents the pages in PagedTileLayout to be remeasured with a different
+            // (incorrect) size to the one used for determining the number of rows and then the
+            // number of pages.
+            if (child != mQSPanelContainer) {
+                super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
+                        parentHeightMeasureSpec, heightUsed);
+            }
         }
     }
 
@@ -151,6 +172,7 @@
         updateClippingPath();
     }
 
+    @Nullable
     public NonInterceptingScrollView getQSPanelContainer() {
         return mQSPanelContainer;
     }
@@ -172,11 +194,19 @@
                                     .getDimensionPixelSize(
                                             R.dimen.large_screen_shade_header_height);
         }
-        mQSPanelContainer.setPaddingRelative(
-                mQSPanelContainer.getPaddingStart(),
-                mSceneContainerEnabled ? 0 : topPadding,
-                mQSPanelContainer.getPaddingEnd(),
-                mQSPanelContainer.getPaddingBottom());
+        if (mQSPanelContainer != null) {
+            mQSPanelContainer.setPaddingRelative(
+                    mQSPanelContainer.getPaddingStart(),
+                    mSceneContainerEnabled ? 0 : topPadding,
+                    mQSPanelContainer.getPaddingEnd(),
+                    mQSPanelContainer.getPaddingBottom());
+        } else {
+            mQSPanel.setPaddingRelative(
+                    mQSPanel.getPaddingStart(),
+                    mSceneContainerEnabled ? 0 : topPadding,
+                    mQSPanel.getPaddingEnd(),
+                    mQSPanel.getPaddingBottom());
+        }
 
         int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin);
         int horizontalPadding = getResources().getDimensionPixelSize(
@@ -220,7 +250,9 @@
 
     public void setExpansion(float expansion) {
         mQsExpansion = expansion;
-        mQSPanelContainer.setScrollingEnabled(expansion > 0f);
+        if (mQSPanelContainer != null) {
+            mQSPanelContainer.setScrollingEnabled(expansion > 0f);
+        }
         updateExpansion();
     }
 
@@ -239,7 +271,7 @@
                 lp.rightMargin = mHorizontalMargins;
                 lp.leftMargin = mHorizontalMargins;
             }
-            if (view == mQSPanelContainer) {
+            if (view == mQSPanelContainer || view == mQSPanel) {
                 // QS panel lays out some of its content full width
                 qsPanelController.setContentMargins(mContentHorizontalPadding,
                         mContentHorizontalPadding);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index 7b001c7..ffbc560 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -81,6 +81,9 @@
     public void onInit() {
         mQuickStatusBarHeaderController.init();
         mView.setSceneContainerEnabled(mSceneContainerEnabled);
+        if (mSceneContainerEnabled && mQsPanelController != null) {
+            mQSPanelContainer.setOnTouchListener(null);
+        }
     }
 
     public void setListening(boolean listening) {
@@ -91,13 +94,17 @@
     protected void onViewAttached() {
         mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController);
         mConfigurationController.addCallback(mConfigurationListener);
-        mQSPanelContainer.setOnTouchListener(mContainerTouchHandler);
+        if (!mSceneContainerEnabled && mQSPanelContainer != null) {
+            mQSPanelContainer.setOnTouchListener(mContainerTouchHandler);
+        }
     }
 
     @Override
     protected void onViewDetached() {
         mConfigurationController.removeCallback(mConfigurationListener);
-        mQSPanelContainer.setOnTouchListener(null);
+        if (mQSPanelContainer != null) {
+            mQSPanelContainer.setOnTouchListener(null);
+        }
     }
 
     public QSContainerImpl getView() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index 7f91fd2..290821e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -61,6 +61,7 @@
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
@@ -171,8 +172,11 @@
     private CommandQueue mCommandQueue;
 
     private View mRootView;
+    @Nullable
     private View mFooterActionsView;
 
+    private final SceneContainerFlags mSceneContainerFlags;
+
     @Inject
     public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
             SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
@@ -185,7 +189,8 @@
             FooterActionsViewModel.Factory footerActionsViewModelFactory,
             FooterActionsViewBinder footerActionsViewBinder,
             LargeScreenShadeInterpolator largeScreenShadeInterpolator,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags,
+            SceneContainerFlags sceneContainerFlags) {
         mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
         mQsMediaHost = qsMediaHost;
         mQqsMediaHost = qqsMediaHost;
@@ -201,6 +206,7 @@
         mFooterActionsViewModelFactory = footerActionsViewModelFactory;
         mFooterActionsViewBinder = footerActionsViewBinder;
         mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
+        mSceneContainerFlags = sceneContainerFlags;
     }
 
     /**
@@ -216,10 +222,17 @@
         mQSPanelController.init();
         mQuickQSPanelController.init();
 
-        mQSFooterActionsViewModel = mFooterActionsViewModelFactory
-                .create(mListeningAndVisibilityLifecycleOwner);
-        bindFooterActionsView(mRootView);
-        mFooterActionsController.init();
+        if (!mSceneContainerFlags.isEnabled()) {
+            mQSFooterActionsViewModel = mFooterActionsViewModelFactory
+                    .create(mListeningAndVisibilityLifecycleOwner);
+            bindFooterActionsView(mRootView);
+            mFooterActionsController.init();
+        } else {
+            View footerView = mRootView.findViewById(R.id.qs_footer_actions);
+            if (footerView != null) {
+                ((ViewGroup) footerView.getParent()).removeView(footerView);
+            }
+        }
 
         mQSPanelScrollView = mRootView.findViewById(R.id.expanded_qs_scroll_view);
         mQSPanelScrollView.addOnLayoutChangeListener(
@@ -234,6 +247,7 @@
                         mScrollListener.onQsPanelScrollChanged(scrollY);
                     }
                 });
+        mQSPanelScrollView.setScrollingEnabled(!mSceneContainerFlags.isEnabled());
         mHeader = mRootView.findViewById(R.id.header);
         mFooter = qsComponent.getQSFooter();
 
@@ -481,7 +495,9 @@
         boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing
                 || mHeaderAnimating || mShowCollapsedOnKeyguard);
         mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
-        mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
+        if (mFooterActionsView != null) {
+            mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
+        }
         mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
                 || (mQsExpanded && !mStackScrollerOverscrolling));
         mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
@@ -622,8 +638,13 @@
 
     @Override
     public int getHeightDiff() {
-        return mQSPanelScrollView.getBottom() - mHeader.getBottom()
-                + mHeader.getPaddingBottom();
+        if (mSceneContainerFlags.isEnabled()) {
+            return mQSPanelController.getViewBottom() - mHeader.getBottom()
+                    + mHeader.getPaddingBottom();
+        } else {
+            return mQSPanelScrollView.getBottom() - mHeader.getBottom()
+                    + mHeader.getPaddingBottom();
+        }
     }
 
     @Override
@@ -678,25 +699,29 @@
         mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
         float footerActionsExpansion =
                 onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
-        mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
-                mInSplitShade);
+        if (mQSFooterActionsViewModel != null) {
+            mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
+                    mInSplitShade);
+        }
         mQSPanelController.setRevealExpansion(expansion);
         mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
         mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
 
-        float qsScrollViewTranslation =
-                onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0;
-        mQSPanelScrollView.setTranslationY(qsScrollViewTranslation);
+        if (!mSceneContainerFlags.isEnabled()) {
+            float qsScrollViewTranslation =
+                    onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0;
+            mQSPanelScrollView.setTranslationY(qsScrollViewTranslation);
 
-        if (fullyCollapsed) {
-            mQSPanelScrollView.setScrollY(0);
-        }
+            if (fullyCollapsed) {
+                mQSPanelScrollView.setScrollY(0);
+            }
 
-        if (!fullyExpanded) {
-            // Set bounds on the QS panel so it doesn't run over the header when animating.
-            mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY();
-            mQsBounds.right = mQSPanelScrollView.getWidth();
-            mQsBounds.bottom = mQSPanelScrollView.getHeight();
+            if (!fullyExpanded) {
+                // Set bounds on the QS panel so it doesn't run over the header when animating.
+                mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY();
+                mQsBounds.right = mQSPanelScrollView.getWidth();
+                mQsBounds.bottom = mQSPanelScrollView.getHeight();
+            }
         }
         updateQsBounds();
 
@@ -786,15 +811,17 @@
             mQsBounds.set(-sideMargin, 0, mQSPanelScrollView.getWidth() + sideMargin,
                     mQSPanelScrollView.getHeight());
         }
-        mQSPanelScrollView.setClipBounds(mQsBounds);
+        if (!mSceneContainerFlags.isEnabled()) {
+            mQSPanelScrollView.setClipBounds(mQsBounds);
 
-        mQSPanelScrollView.getLocationOnScreen(mLocationTemp);
-        int left = mLocationTemp[0];
-        int top = mLocationTemp[1];
-        mQsMediaHost.getCurrentClipping().set(left, top,
-                left + getView().getMeasuredWidth(),
-                top + mQSPanelScrollView.getMeasuredHeight()
-                        - mQSPanelController.getPaddingBottom());
+            mQSPanelScrollView.getLocationOnScreen(mLocationTemp);
+            int left = mLocationTemp[0];
+            int top = mLocationTemp[1];
+            mQsMediaHost.getCurrentClipping().set(left, top,
+                    left + getView().getMeasuredWidth(),
+                    top + mQSPanelScrollView.getMeasuredHeight()
+                            - mQSPanelController.getPaddingBottom());
+        }
     }
 
     private void updateMediaPositions() {
@@ -867,9 +894,15 @@
         // The customize state changed, so our height changed.
         mContainer.updateExpansion();
         boolean customizing = isCustomizing();
-        mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+        if (mSceneContainerFlags.isEnabled()) {
+            mQSPanelController.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+        } else {
+            mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+        }
         mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
-        mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+        if (mFooterActionsView != null) {
+            mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+        }
         mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
         // Let the panel know the position changed and it needs to update where notifications
         // and whatnot are.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 51b94dd..7a7ee59 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -387,7 +387,7 @@
         setPaddingRelative(getPaddingStart(),
                 mSceneContainerEnabled ? 0 : paddingTop,
                 getPaddingEnd(),
-                paddingBottom);
+                mSceneContainerEnabled ? 0 : paddingBottom);
     }
 
     void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index ef58a60..c3f5086 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -278,5 +278,9 @@
     public int getPaddingBottom() {
         return mView.getPaddingBottom();
     }
+
+    int getViewBottom() {
+        return mView.getBottom();
+    }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index a103566..07705f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -58,6 +58,7 @@
     private final RecyclerView mRecyclerView;
     private boolean mCustomizing;
     private QSContainerController mQsContainerController;
+    private final Toolbar mToolbar;
     private QS mQs;
     private int mX;
     private int mY;
@@ -69,15 +70,15 @@
 
         LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this);
         mClipper = new QSDetailClipper(findViewById(R.id.customize_container));
-        Toolbar toolbar = findViewById(com.android.internal.R.id.action_bar);
+        mToolbar = findViewById(com.android.internal.R.id.action_bar);
         TypedValue value = new TypedValue();
         mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
-        toolbar.setNavigationIcon(
+        mToolbar.setNavigationIcon(
                 getResources().getDrawable(value.resourceId, mContext.getTheme()));
 
-        toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset)
+        mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset)
                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-        toolbar.setTitle(R.string.qs_edit);
+        mToolbar.setTitle(R.string.qs_edit);
         mRecyclerView = findViewById(android.R.id.list);
         mTransparentView = findViewById(R.id.customizer_transparent_view);
         DefaultItemAnimator animator = new DefaultItemAnimator();
@@ -184,6 +185,14 @@
         return isShown;
     }
 
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mToolbar.setTitleTextAppearance(mContext,
+                android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
+        updateToolbarMenuFontSize();
+    }
+
     void setCustomizing(boolean customizing) {
         mCustomizing = customizing;
         if (mQs != null) {
@@ -269,4 +278,11 @@
         lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
         mTransparentView.setLayoutParams(lp);
     }
+
+    private void updateToolbarMenuFontSize() {
+        // Clearing and re-adding the toolbar action force updates the font size
+        mToolbar.getMenu().clear();
+        mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset)
+                .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 4565200..c5eeb2f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -249,6 +249,10 @@
             height = iconSize
             marginEnd = endMargin
         }
+
+        background = createTileBackground()
+        setColor(backgroundColor)
+        setOverlayColor(backgroundOverlayColor)
     }
 
     private fun createAndAddLabels() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index ce840ee..0d43396 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -17,10 +17,13 @@
 package com.android.systemui.qs.ui.adapter
 
 import android.content.Context
+import android.content.pm.ActivityInfo
 import android.os.Bundle
 import android.view.View
 import androidx.annotation.VisibleForTesting
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater
+import com.android.settingslib.applications.InterestingConfigChanges
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
@@ -58,7 +61,7 @@
 
     /**
      * Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in
-     * [qsView]
+     * [qsView]. Re-inflations due to configuration changes will use the last used [context].
      */
     suspend fun inflate(context: Context)
 
@@ -90,6 +93,7 @@
     private val qsImplProvider: Provider<QSImpl>,
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Application applicationScope: CoroutineScope,
+    private val configurationInteractor: ConfigurationInteractor,
     private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
 ) : QSContainerController, QSSceneAdapter {
 
@@ -99,7 +103,15 @@
         qsImplProvider: Provider<QSImpl>,
         @Main dispatcher: CoroutineDispatcher,
         @Application scope: CoroutineScope,
-    ) : this(qsSceneComponentFactory, qsImplProvider, dispatcher, scope, ::AsyncLayoutInflater)
+        configurationInteractor: ConfigurationInteractor,
+    ) : this(
+        qsSceneComponentFactory,
+        qsImplProvider,
+        dispatcher,
+        scope,
+        configurationInteractor,
+        ::AsyncLayoutInflater,
+    )
 
     private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED)
     private val _isCustomizing: MutableStateFlow<Boolean> = MutableStateFlow(false)
@@ -109,14 +121,36 @@
     val qsImpl = _qsImpl.asStateFlow()
     override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull()
 
+    // Same config changes as in FragmentHostManager
+    private val interestingChanges =
+        InterestingConfigChanges(
+            ActivityInfo.CONFIG_FONT_SCALE or
+                ActivityInfo.CONFIG_LOCALE or
+                ActivityInfo.CONFIG_ASSETS_PATHS
+        )
+
     init {
         applicationScope.launch {
-            state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
-                _qsImpl.value?.apply {
-                    if (state != QSSceneAdapter.State.QS && customizing) {
-                        this@apply.closeCustomizerImmediately()
+            launch {
+                state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
+                    _qsImpl.value?.apply {
+                        if (state != QSSceneAdapter.State.QS && customizing) {
+                            this@apply.closeCustomizerImmediately()
+                        }
+                        applyState(state)
                     }
-                    applyState(state)
+                }
+            }
+            launch {
+                configurationInteractor.configurationValues.collect { config ->
+                    if (interestingChanges.applyNewConfig(config)) {
+                        // Assumption: The context is always the same and with the same theme.
+                        // If colors change they will be reflected as attributes in the theme.
+                        qsImpl.value?.view?.let { inflate(it.context) }
+                    } else {
+                        qsImpl.value?.onConfigurationChanged(config)
+                        qsImpl.value?.view?.dispatchConfigurationChanged(config)
+                    }
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index e5e1e84..8a900ece 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -16,7 +16,10 @@
 
 package com.android.systemui.qs.ui.viewmodel
 
+import androidx.lifecycle.LifecycleOwner
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
@@ -24,6 +27,7 @@
 import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
 import kotlinx.coroutines.flow.map
 
@@ -35,6 +39,8 @@
     val shadeHeaderViewModel: ShadeHeaderViewModel,
     val qsSceneAdapter: QSSceneAdapter,
     val notifications: NotificationsPlaceholderViewModel,
+    private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
+    private val footerActionsController: FooterActionsController,
 ) {
     val destinationScenes =
         qsSceneAdapter.isCustomizing.map { customizing ->
@@ -47,4 +53,13 @@
                 )
             }
         }
+
+    private val footerActionsControllerInitialized = AtomicBoolean(false)
+
+    fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
+        if (footerActionsControllerInitialized.compareAndSet(false, true)) {
+            footerActionsController.init()
+        }
+        return footerActionsViewModelFactory.create(lifecycleOwner)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index fd53423..cc53aab 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -169,7 +169,6 @@
     private final DisplayTracker mDisplayTracker;
 
     private Region mActiveNavBarRegion;
-    private SurfaceControl mNavigationBarSurface;
 
     private IOverviewProxy mOverviewProxy;
     private int mConnectionBackoffAttempts;
@@ -488,7 +487,6 @@
                 Log.e(TAG_OPS, "Failed to call onInitialize()", e);
             }
             dispatchNavButtonBounds();
-            dispatchNavigationBarSurface();
 
             // Force-update the systemui state flags
             updateSystemUiStateFlags();
@@ -679,28 +677,6 @@
                 .commitUpdate(mContext.getDisplayId());
     }
 
-    /**
-     * Called when the navigation bar surface is created or changed
-     */
-    public void onNavigationBarSurfaceChanged(SurfaceControl navbarSurface) {
-        mNavigationBarSurface = navbarSurface;
-        dispatchNavigationBarSurface();
-    }
-
-    private void dispatchNavigationBarSurface() {
-        try {
-            if (mOverviewProxy != null) {
-                // Catch all for cases where the surface is no longer valid
-                if (mNavigationBarSurface != null && !mNavigationBarSurface.isValid()) {
-                    mNavigationBarSurface = null;
-                }
-                mOverviewProxy.onNavigationBarSurface(mNavigationBarSurface);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG_OPS, "Failed to notify back action", e);
-        }
-    }
-
     private void updateEnabledAndBinding() {
         updateEnabledState();
         startConnectionToCurrentUser();
@@ -1075,7 +1051,6 @@
         pw.print("  mInputFocusTransferStartY="); pw.println(mInputFocusTransferStartY);
         pw.print("  mInputFocusTransferStartMillis="); pw.println(mInputFocusTransferStartMillis);
         pw.print("  mActiveNavBarRegion="); pw.println(mActiveNavBarRegion);
-        pw.print("  mNavigationBarSurface="); pw.println(mNavigationBarSurface);
         pw.print("  mNavBarMode="); pw.println(mNavBarMode);
         mSysUiState.dump(pw, args);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 782d651..3362ebc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -55,7 +55,15 @@
      * The width of the area in which a right edge swipe can open the hub, in pixels. Read from
      * resources when [initView] is called.
      */
-    private var edgeSwipeRegionWidth: Int = 0
+    // TODO(b/320786721): support RTL layouts
+    private var rightEdgeSwipeRegionWidth: Int = 0
+
+    /**
+     * The height of the area in which a top edge swipe while the hub is open will not intercept
+     * touches, in pixels. This allows the top edge swipe to instead open the notification shade.
+     * Read from resources when [initView] is called.
+     */
+    private var topEdgeSwipeRegionWidth: Int = 0
 
     /**
      * True if we are currently tracking a gesture for opening the hub that started in the edge
@@ -63,6 +71,9 @@
      */
     private var isTrackingOpenGesture = false
 
+    /** True if we are currently tracking a touch on the hub while it's open. */
+    private var isTrackingHubTouch = false
+
     /**
      * True if the hub UI is fully open, meaning it should receive touch input.
      *
@@ -113,8 +124,14 @@
 
         communalContainerView = containerView
 
-        edgeSwipeRegionWidth =
-            communalContainerView.resources.getDimensionPixelSize(R.dimen.communal_grid_gutter_size)
+        rightEdgeSwipeRegionWidth =
+            communalContainerView.resources.getDimensionPixelSize(
+                R.dimen.communal_right_edge_swipe_region_width
+            )
+        topEdgeSwipeRegionWidth =
+            communalContainerView.resources.getDimensionPixelSize(
+                R.dimen.communal_top_edge_swipe_region_height
+            )
 
         collectFlow(
             communalContainerView,
@@ -157,17 +174,38 @@
         //  fully showing state
         val hubOccluded = anyBouncerShowing || shadeShowing
 
-        // If the hub is fully visible, send all touch events to it.
-        val communalVisible = hubShowing && !hubOccluded
-        if (communalVisible) {
+        // If the hub is fully visible, send all touch events to it, other than top and bottom edge
+        // swipes.
+        if (hubShowing && isDown) {
+            val y = ev.rawY
+            val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth
+
+            // TODO(b/315207481): allow bottom edge swipes to open the bouncer
+            if (topSwipe) {
+                // Don't intercept touches at the top edge so that swipes can open the notification
+                // shade.
+                return false
+            }
+
+            if (!hubOccluded) {
+                isTrackingHubTouch = true
+                dispatchTouchEvent(ev)
+                // Return true regardless of dispatch result as some touches at the start of a
+                // gesture may return false from dispatchTouchEvent.
+                return true
+            }
+        } else if (isTrackingHubTouch) {
+            if (isUp || isCancel) {
+                isTrackingHubTouch = false
+            }
             dispatchTouchEvent(ev)
             // Return true regardless of dispatch result as some touches at the start of a gesture
             // may return false from dispatchTouchEvent.
             return true
         }
 
-        if (edgeSwipeRegionWidth == 0) {
-            // If the edge region width has not been read yet or whatever reason, don't bother
+        if (rightEdgeSwipeRegionWidth == 0) {
+            // If the edge region width has not been read yet for whatever reason, don't bother
             // intercepting touches to open the hub.
             return false
         }
@@ -175,7 +213,7 @@
         if (!isTrackingOpenGesture && isDown) {
             val x = ev.rawX
             val inOpeningSwipeRegion: Boolean =
-                x >= communalContainerView.width - edgeSwipeRegionWidth
+                x >= communalContainerView.width - rightEdgeSwipeRegionWidth
             if (inOpeningSwipeRegion && !hubOccluded) {
                 isTrackingOpenGesture = true
                 dispatchTouchEvent(ev)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 60feb82..0053474 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -50,6 +50,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -118,6 +119,7 @@
     private final Lazy<SelectedUserInteractor> mUserInteractor;
     private final Lazy<ShadeInteractor> mShadeInteractorLazy;
     private final SceneContainerFlags mSceneContainerFlags;
+    private final Lazy<CommunalInteractor> mCommunalInteractor;
     private ViewGroup mWindowRootView;
     private LayoutParams mLp;
     private boolean mHasTopUi;
@@ -165,7 +167,8 @@
             ShadeWindowLogger logger,
             Lazy<SelectedUserInteractor> userInteractor,
             UserTracker userTracker,
-            SceneContainerFlags sceneContainerFlags) {
+            SceneContainerFlags sceneContainerFlags,
+            Lazy<CommunalInteractor> communalInteractor) {
         mContext = context;
         mWindowRootViewComponentFactory = windowRootViewComponentFactory;
         mWindowManager = windowManager;
@@ -184,6 +187,7 @@
         mAuthController = authController;
         mUserInteractor = userInteractor;
         mSceneContainerFlags = sceneContainerFlags;
+        mCommunalInteractor = communalInteractor;
         mLastKeyguardRotationAllowed = mKeyguardStateController.isKeyguardScreenRotationAllowed();
         mLockScreenDisplayTimeout = context.getResources()
                 .getInteger(R.integer.config_lockScreenDisplayTimeout);
@@ -325,6 +329,11 @@
                 mShadeInteractorLazy.get().isQsExpanded(),
                 this::onQsExpansionChanged
         );
+        collectFlow(
+                mWindowRootView,
+                mCommunalInteractor.get().isCommunalShowing(),
+                this::onCommunalShowingChanged
+        );
     }
 
     @Override
@@ -501,14 +510,21 @@
     }
 
     private void applyUserActivityTimeout(NotificationShadeWindowState state) {
-        if (state.isKeyguardShowingAndNotOccluded()
+        final Boolean communalShowing = state.isCommunalShowingAndNotOccluded();
+        final Boolean keyguardShowing = state.isKeyguardShowingAndNotOccluded();
+        long timeout = -1;
+        if ((communalShowing || keyguardShowing)
                 && state.statusBarState == StatusBarState.KEYGUARD
                 && !state.qsExpanded) {
-            mLpChanged.userActivityTimeout = state.bouncerShowing
-                    ? KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS : mLockScreenDisplayTimeout;
-        } else {
-            mLpChanged.userActivityTimeout = -1;
+            if (state.bouncerShowing) {
+                timeout = KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS;
+            } else if (communalShowing) {
+                timeout = CommunalInteractor.AWAKE_INTERVAL_MS;
+            } else if (keyguardShowing) {
+                timeout = mLockScreenDisplayTimeout;
+            }
         }
+        mLpChanged.userActivityTimeout = timeout;
     }
 
     private void applyInputFeatures(NotificationShadeWindowState state) {
@@ -607,7 +623,8 @@
                 state.forcePluginOpen,
                 state.dozing,
                 state.scrimsVisibility,
-                state.backgroundBlurRadius
+                state.backgroundBlurRadius,
+                state.communalShowing
         );
     }
 
@@ -731,6 +748,12 @@
         apply(mCurrentState);
     }
 
+    @VisibleForTesting
+    void onCommunalShowingChanged(Boolean showing) {
+        mCurrentState.communalShowing = showing;
+        apply(mCurrentState);
+    }
+
     @Override
     public void setForceUserActivity(boolean forceUserActivity) {
         mCurrentState.forceUserActivity = forceUserActivity;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
index 0b20170..f9c9d83 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -58,12 +58,17 @@
     @JvmField var dreaming: Boolean = false,
     @JvmField var scrimsVisibility: Int = 0,
     @JvmField var backgroundBlurRadius: Int = 0,
+    @JvmField var communalShowing: Boolean = false,
 ) {
 
     fun isKeyguardShowingAndNotOccluded(): Boolean {
         return keyguardShowing && !keyguardOccluded
     }
 
+    fun isCommunalShowingAndNotOccluded(): Boolean {
+        return communalShowing && !keyguardOccluded
+    }
+
     /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */
     val asStringList: List<String> by lazy {
         listOf(
@@ -93,7 +98,8 @@
             forcePluginOpen.toString(),
             dozing.toString(),
             scrimsVisibility.toString(),
-            backgroundBlurRadius.toString()
+            backgroundBlurRadius.toString(),
+            communalShowing.toString(),
         )
     }
 
@@ -134,6 +140,7 @@
             dozing: Boolean,
             scrimsVisibility: Int,
             backgroundBlurRadius: Int,
+            communalShowing: Boolean,
         ) {
             buffer.advance().apply {
                 this.keyguardShowing = keyguardShowing
@@ -165,6 +172,7 @@
                 this.dozing = dozing
                 this.scrimsVisibility = scrimsVisibility
                 this.backgroundBlurRadius = backgroundBlurRadius
+                this.communalShowing = communalShowing
             }
         }
 
@@ -209,7 +217,8 @@
                 "forcePluginOpen",
                 "dozing",
                 "scrimsVisibility",
-                "backgroundBlurRadius"
+                "backgroundBlurRadius",
+                "communalShowing"
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 46806e6..54b6ad7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -790,7 +790,7 @@
 
 /** Mutates the HeadsUp state of notifications. */
 private interface HunMutator {
-    fun updateNotification(key: String, alert: Boolean)
+    fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
     fun removeNotification(key: String, releaseImmediately: Boolean)
 }
 
@@ -801,8 +801,8 @@
 private class HunMutatorImpl(private val headsUpManager: HeadsUpManager) : HunMutator {
     private val deferred = mutableListOf<Pair<String, Boolean>>()
 
-    override fun updateNotification(key: String, alert: Boolean) {
-        headsUpManager.updateNotification(key, alert)
+    override fun updateNotification(key: String, shouldHeadsUpAgain: Boolean) {
+        headsUpManager.updateNotification(key, shouldHeadsUpAgain)
     }
 
     override fun removeNotification(key: String, releaseImmediately: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
index 380cdad..ae4ba27 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
@@ -18,6 +18,7 @@
 
 import android.os.UserHandle
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Flags.screenshareNotificationHiding
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
@@ -30,6 +31,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import dagger.Binds
 import dagger.Module
@@ -55,6 +57,8 @@
     private val statusBarStateController: StatusBarStateController,
     private val keyguardStateController: KeyguardStateController,
     private val selectedUserInteractor: SelectedUserInteractor,
+    private val sensitiveNotificationProtectionController:
+        SensitiveNotificationProtectionController,
 ) : Invalidator("SensitiveContentInvalidator"),
         SensitiveContentCoordinator,
         DynamicPrivacyController.Listener,
@@ -82,10 +86,13 @@
             return
         }
 
+        val isSensitiveContentProtectionActive = screenshareNotificationHiding() &&
+            sensitiveNotificationProtectionController.isSensitiveStateActive
         val currentUserId = lockscreenUserManager.currentUserId
         val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId)
-        val deviceSensitive = devicePublic &&
-                !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)
+        val deviceSensitive = (devicePublic &&
+                !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) ||
+                isSensitiveContentProtectionActive
         val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked
         for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) {
             val notifUserId = entry.sbn.user.identifier
@@ -105,9 +112,13 @@
                     else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
                 }
             }
+
+            val shouldProtectNotification = screenshareNotificationHiding() &&
+                sensitiveNotificationProtectionController.shouldProtectNotification(entry)
+
             val needsRedaction = lockscreenUserManager.needsRedaction(entry)
             val isSensitive = userPublic && needsRedaction
-            entry.setSensitive(isSensitive, deviceSensitive)
+            entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index 0bc8e68..f375ebc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -187,7 +187,7 @@
             configuration.getDimensionPixelSize(RInternal.dimen.status_bar_icon_size_sp)
         val iconHorizontalPaddingFlow: Flow<Int> =
             configuration.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
-        val layoutParams: Flow<FrameLayout.LayoutParams> =
+        val layoutParams: StateFlow<FrameLayout.LayoutParams> =
             combine(iconSizeFlow, iconHorizontalPaddingFlow, systemBarUtilsState.statusBarHeight) {
                     iconSize,
                     iconHPadding,
@@ -206,7 +206,7 @@
 
     private suspend fun Flow<NotificationIconsViewData>.bindIcons(
         view: NotificationIconContainer,
-        layoutParams: Flow<FrameLayout.LayoutParams>,
+        layoutParams: StateFlow<FrameLayout.LayoutParams>,
         notifyBindingFailures: (Collection<String>) -> Unit,
         viewStore: IconViewStore,
         bindIcon: suspend (iconKey: String, view: StatusBarIconView) -> Unit,
@@ -259,7 +259,7 @@
                             // added again.
                             removeTransientView(sbiv)
                         }
-                        view.addView(sbiv)
+                        view.addView(sbiv, layoutParams.value)
                         boundViewsByNotifKey.remove(notifKey)?.second?.cancel()
                         boundViewsByNotifKey[notifKey] =
                             Pair(
@@ -267,7 +267,9 @@
                                 launch {
                                     launch {
                                         layoutParams.collectTracingEach("SBIV#bindLayoutParams") {
-                                            sbiv.layoutParams = it
+                                            if (it != sbiv.layoutParams) {
+                                                sbiv.layoutParams = it
+                                            }
                                         }
                                     }
                                     bindIcon(notifKey, sbiv)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
index 3a59978..46ddba4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
@@ -65,9 +65,7 @@
             CallLayoutSetDataAsyncFactory callLayoutSetDataAsyncFactory
     ) {
         final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>();
-        if (featureFlags.isEnabled(Flags.PRECOMPUTED_TEXT)) {
-            replacementFactories.add(precomputedTextViewFactory);
-        }
+        replacementFactories.add(precomputedTextViewFactory);
         if (featureFlags.isEnabled(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)) {
             replacementFactories.add(bigPictureLayoutInflaterFactory);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt
new file mode 100644
index 0000000..a21dd9b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notification avalanche suppression flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationAvalancheSuppression {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationAvalancheSuppression()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+            RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
\ No newline at end of file
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 6a66bb7..1143481 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
@@ -22,6 +22,7 @@
 import static com.android.app.animation.Interpolators.STANDARD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
+import static com.android.systemui.Flags.screenshareNotificationHiding;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener;
@@ -135,6 +136,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.tuner.TunerService;
@@ -218,6 +220,8 @@
     private final SecureSettings mSecureSettings;
     private final NotificationDismissibilityProvider mDismissibilityProvider;
     private final ActivityStarter mActivityStarter;
+    private final SensitiveNotificationProtectionController
+            mSensitiveNotificationProtectionController;
 
     private View mLongPressedView;
 
@@ -295,6 +299,15 @@
                 }
             };
 
+    private final Runnable mSensitiveStateChangedListener = new Runnable() {
+        @Override
+        public void run() {
+            // Animate false to protect against screen recording capturing content
+            // during the animation
+            updateSensitivenessWithAnimation(false);
+        }
+    };
+
     private final DynamicPrivacyController.Listener mDynamicPrivacyControllerListener = () -> {
         if (mView.isExpanded()) {
             // The bottom might change because we're using the final actual height of the view
@@ -342,6 +355,12 @@
     private float mMaxAlphaForExpansion = 1.0f;
     private float mMaxAlphaForUnhide = 1.0f;
 
+    /**
+     * Maximum alpha when to and from or sitting idle on the glanceable hub. Will be 1.0f when the
+     * hub is not visible or transitioning.
+     */
+    private float mMaxAlphaForGlanceableHub = 1.0f;
+
     private final NotificationListViewBinder mViewBinder;
 
     private void updateResources() {
@@ -399,7 +418,20 @@
     }
 
     private void updateSensitivenessWithAnimation(boolean animate) {
-        mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode());
+        Trace.beginSection("NSSLC.updateSensitivenessWithAnimation");
+        if (screenshareNotificationHiding()) {
+            boolean isAnyProfilePublic = mLockscreenUserManager.isAnyProfilePublicMode();
+            boolean isSensitiveContentProtectionActive =
+                    mSensitiveNotificationProtectionController.isSensitiveStateActive();
+            boolean isSensitive = isAnyProfilePublic || isSensitiveContentProtectionActive;
+
+            // Only animate if in a non-sensitive state (not screen sharing)
+            boolean shouldAnimate = animate && !isSensitiveContentProtectionActive;
+            mView.updateSensitiveness(shouldAnimate, isSensitive);
+        } else {
+            mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode());
+        }
+        Trace.endSection();
     }
 
     /**
@@ -708,7 +740,8 @@
             SecureSettings secureSettings,
             NotificationDismissibilityProvider dismissibilityProvider,
             ActivityStarter activityStarter,
-            SplitShadeStateController splitShadeStateController) {
+            SplitShadeStateController splitShadeStateController,
+            SensitiveNotificationProtectionController sensitiveNotificationProtectionController) {
         mView = view;
         mKeyguardTransitionRepo = keyguardTransitionRepo;
         mViewBinder = viewBinder;
@@ -756,6 +789,7 @@
         mSecureSettings = secureSettings;
         mDismissibilityProvider = dismissibilityProvider;
         mActivityStarter = activityStarter;
+        mSensitiveNotificationProtectionController = sensitiveNotificationProtectionController;
         mView.passSplitShadeStateController(splitShadeStateController);
         mDumpManager.registerDumpable(this);
         updateResources();
@@ -860,6 +894,11 @@
         mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
         mDeviceProvisionedListener.onDeviceProvisionedChanged();
 
+        if (screenshareNotificationHiding()) {
+            mSensitiveNotificationProtectionController
+                    .registerSensitiveStateListener(mSensitiveStateChangedListener);
+        }
+
         if (mView.isAttachedToWindow()) {
             mOnAttachStateChangeListener.onViewAttachedToWindow(mView);
         }
@@ -1252,9 +1291,19 @@
         updateAlpha();
     }
 
+    /**
+     * Sets the max alpha value for notifications when idle on the glanceable hub or when
+     * transitioning to/from the glanceable hub.
+     */
+    public void setMaxAlphaForGlanceableHub(float alpha) {
+        mMaxAlphaForGlanceableHub = alpha;
+        updateAlpha();
+    }
+
     private void updateAlpha() {
         if (mView != null) {
-            mView.setAlpha(Math.min(mMaxAlphaForExpansion, mMaxAlphaForUnhide));
+            mView.setAlpha(Math.min(mMaxAlphaForExpansion,
+                    Math.min(mMaxAlphaForUnhide, mMaxAlphaForGlanceableHub)));
         }
     }
 
@@ -1746,6 +1795,7 @@
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("mMaxAlphaForExpansion=" + mMaxAlphaForExpansion);
         pw.println("mMaxAlphaForUnhide=" + mMaxAlphaForUnhide);
+        pw.println("mMaxAlphaForGlanceableHub=" + mMaxAlphaForGlanceableHub);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 12927b8..f842e30 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -18,7 +18,6 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.dagger.qualifiers.Main
@@ -125,7 +124,14 @@
 
                     launch { viewModel.translationY.collect { controller.setTranslationY(it) } }
 
-                    launch { viewModel.alpha.collect { controller.setMaxAlphaForExpansion(it) } }
+                    launch {
+                        viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) }
+                    }
+                    launch {
+                        viewModel.glanceableHubAlpha.collect {
+                            controller.setMaxAlphaForGlanceableHub(it)
+                        }
+                    }
                 }
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index a48fb45..99cd89b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -20,6 +20,7 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
 import com.android.systemui.common.shared.model.NotificationContainerBounds
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -27,6 +28,8 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -61,8 +64,11 @@
     private val keyguardInteractor: KeyguardInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val shadeInteractor: ShadeInteractor,
+    communalInteractor: CommunalInteractor,
     occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
     lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
+    glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
+    lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel
 ) {
     private val statesForConstrainedNotifications =
         setOf(
@@ -87,6 +93,20 @@
             .distinctUntilChanged()
             .onStart { emit(false) }
 
+    private val lockscreenToGlanceableHubRunning =
+        keyguardTransitionInteractor
+            .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+            .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+            .distinctUntilChanged()
+            .onStart { emit(false) }
+
+    private val glanceableHubToLockscreenRunning =
+        keyguardTransitionInteractor
+            .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN)
+            .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+            .distinctUntilChanged()
+            .onStart { emit(false) }
+
     val shadeCollapseFadeInComplete = MutableStateFlow(false)
 
     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
@@ -144,6 +164,24 @@
                 initialValue = false,
             )
 
+    /** Are we purely on the glanceable hub without the shade/qs? */
+    internal val isOnGlanceableHubWithoutShade: Flow<Boolean> =
+        combine(
+                communalInteractor.isIdleOnCommunal,
+                // Shade with notifications
+                shadeInteractor.shadeExpansion.map { it > 0f },
+                // Shade without notifications, quick settings only (pull down from very top on
+                // lockscreen)
+                shadeInteractor.qsExpansion.map { it > 0f },
+            ) { isIdleOnCommunal, isShadeVisible, qsExpansion ->
+                isIdleOnCommunal && !(isShadeVisible || qsExpansion)
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
+
     /** Fade in only for use after the shade collapses */
     val shadeCollpaseFadeIn: Flow<Boolean> =
         flow {
@@ -201,7 +239,7 @@
                 initialValue = NotificationContainerBounds(),
             )
 
-    val alpha: Flow<Float> =
+    val expansionAlpha: Flow<Float> =
         // Due to issues with the legacy shade, some shade expansion events are sent incorrectly,
         // such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition
         // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while
@@ -235,6 +273,43 @@
         }
 
     /**
+     * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition
+     * or idle on the glanceable hub.
+     *
+     * Must return 1.0f when not controlling the alpha since notifications does a min of all the
+     * alpha sources.
+     */
+    val glanceableHubAlpha: Flow<Float> =
+        isOnGlanceableHubWithoutShade.flatMapLatest { isOnGlanceableHubWithoutShade ->
+            combineTransform(
+                lockscreenToGlanceableHubRunning,
+                glanceableHubToLockscreenRunning,
+                merge(
+                        lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
+                        glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
+                    )
+                    .onStart {
+                        // Transition flows don't emit a value on start, kick things off so the
+                        // combine starts.
+                        emit(1f)
+                    }
+            ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha ->
+                if (isOnGlanceableHubWithoutShade) {
+                    // Notifications should not be visible on the glanceable hub.
+                    // TODO(b/321075734): implement a way to actually set the notifications to gone
+                    //  while on the hub instead of just adjusting alpha
+                    emit(0f)
+                } else if (lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning) {
+                    emit(alpha)
+                } else {
+                    // Not on the hub and no transitions running, return full visibility so we don't
+                    // block the notifications from showing.
+                    emit(1f)
+                }
+            }
+        }
+
+    /**
      * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be
      * translated as the keyguard fades out.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index ae04eaf..aabe4a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -60,6 +60,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.ScrimAlpha;
 import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -217,6 +218,7 @@
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private final KeyguardInteractor mKeyguardInteractor;
 
     private GradientColors mColors;
     private boolean mNeedsDrawableColorUpdate;
@@ -298,7 +300,7 @@
             DozeParameters dozeParameters,
             AlarmManager alarmManager,
             KeyguardStateController keyguardStateController,
-            DelayedWakeLock.Builder delayedWakeLockBuilder,
+            DelayedWakeLock.Factory delayedWakeLockFactory,
             Handler handler,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             DockManager dockManager,
@@ -311,6 +313,7 @@
             PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
             AlternateBouncerToGoneTransitionViewModel alternateBouncerToGoneTransitionViewModel,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
+            KeyguardInteractor keyguardInteractor,
             WallpaperRepository wallpaperRepository,
             @Main CoroutineDispatcher mainDispatcher,
             LargeScreenShadeInterpolator largeScreenShadeInterpolator) {
@@ -328,7 +331,7 @@
         mScreenOffAnimationController = screenOffAnimationController;
         mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout,
                 "hide_aod_wallpaper", mHandler);
-        mWakeLock = delayedWakeLockBuilder.setHandler(mHandler).setTag("Scrims").build();
+        mWakeLock = delayedWakeLockFactory.create("Scrims");
         // Scrim alpha is initially set to the value on the resource but might be changed
         // to make sure that text on top of it is legible.
         mDozeParameters = dozeParameters;
@@ -357,6 +360,7 @@
         mPrimaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel;
         mAlternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel;
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+        mKeyguardInteractor = keyguardInteractor;
         mWallpaperRepository = wallpaperRepository;
         mMainDispatcher = mainDispatcher;
     }
@@ -759,7 +763,9 @@
             // see: b/186644628
             mNotificationsScrim.setDrawableBounds(left - 1, top, right + 1, bottom);
             mScrimBehind.setBottomEdgePosition((int) top);
+            mKeyguardInteractor.setTopClippingBounds((int) top);
         } else {
+            mKeyguardInteractor.setTopClippingBounds(null);
             mNotificationsScrim.setDrawableBounds(left, top, right, bottom);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt
new file mode 100644
index 0000000..252945f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.dagger
+
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import javax.inject.Qualifier
+
+/** Detailed [DeviceBasedSatelliteRepository] logs */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class OemSatelliteInputLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index e309c32..2b90e64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -265,6 +265,13 @@
             return factory.create("VerboseMobileViewLog", 100)
         }
 
+        @Provides
+        @SysUISingleton
+        @OemSatelliteInputLog
+        fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer {
+            return factory.create("DeviceBasedSatelliteInputLog", 32)
+        }
+
         const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
             "FirstMobileSubShowingNetworkTypeIcon"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
index e3c3139..8400fb0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.satellite.ui.DeviceBasedSatelliteBindableIcon
 import javax.inject.Inject
 
 /**
@@ -38,11 +39,12 @@
 class BindableIconsRegistryImpl
 @Inject
 constructor(
-/** Bindables go here */
+    /** Bindables go here */
+    oemSatellite: DeviceBasedSatelliteBindableIcon
 ) : BindableIconsRegistry {
     /**
      * Adding the injected bindables to this list will get them registered with
      * StatusBarIconController
      */
-    override val bindableIcons: List<BindableIcon> = listOf()
+    override val bindableIcons: List<BindableIcon> = listOf(oemSatellite)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index bc38b53..a608be3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -72,6 +72,9 @@
      */
     val isInService: StateFlow<Boolean>
 
+    /** Reflects [android.telephony.ServiceState.isUsingNonTerrestrialNetwork] */
+    val isNonTerrestrial: StateFlow<Boolean>
+
     /** True if [android.telephony.SignalStrength] told us that this connection is using GSM */
     val isGsm: StateFlow<Boolean>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
index b2a7733..6de7a00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_GSM
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_IN_SERVICE
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_NTN
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING
@@ -109,6 +110,17 @@
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value)
 
+    private val _isNonTerrestrial = MutableStateFlow(false)
+    override val isNonTerrestrial =
+        _isNonTerrestrial
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = COL_IS_NTN,
+                _isNonTerrestrial.value
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), _isNonTerrestrial.value)
+
     private val _isGsm = MutableStateFlow(false)
     override val isGsm =
         _isGsm
@@ -227,6 +239,7 @@
             (event.activity ?: TelephonyManager.DATA_ACTIVITY_NONE).toMobileDataActivityModel()
         _carrierNetworkChangeActive.value = event.carrierNetworkChange
         _resolvedNetworkType.value = resolvedNetworkType
+        _isNonTerrestrial.value = event.ntn
 
         isAllowedDuringAirplaneMode.value = false
         hasPrioritizedNetworkCapabilities.value = event.slice
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
index 4cd877e..11a61a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
@@ -77,6 +77,7 @@
         val roaming = getString("roam") == "show"
         val name = getString("networkname") ?: "demo mode"
         val slice = getString("slice").toBoolean()
+        val ntn = getString("ntn").toBoolean()
 
         return Mobile(
             level = level,
@@ -89,6 +90,7 @@
             roaming = roaming,
             name = name,
             slice = slice,
+            ntn = ntn,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
index 0aa95f8..4836abe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
@@ -37,6 +37,7 @@
         val roaming: Boolean,
         val name: String,
         val slice: Boolean = false,
+        val ntn: Boolean = false,
     ) : FakeNetworkEventModel
 
     data class MobileDisabled(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
index e5a5695..f8858c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -168,6 +168,7 @@
     override val isEmergencyOnly = MutableStateFlow(false).asStateFlow()
     override val operatorAlphaShort = MutableStateFlow(null).asStateFlow()
     override val isInService = MutableStateFlow(true).asStateFlow()
+    override val isNonTerrestrial = MutableStateFlow(false).asStateFlow()
     override val isGsm = MutableStateFlow(false).asStateFlow()
     override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow()
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index 48bf7ac..a124196 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -175,6 +175,21 @@
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value)
 
+    override val isNonTerrestrial =
+        activeRepo
+            .flatMapLatest { it.isNonTerrestrial }
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = COL_IS_NTN,
+                activeRepo.value.isNonTerrestrial.value
+            )
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                activeRepo.value.isNonTerrestrial.value
+            )
+
     override val isGsm =
         activeRepo
             .flatMapLatest { it.isGsm }
@@ -366,6 +381,7 @@
         const val COL_CARRIER_NETWORK_CHANGE = "carrierNetworkChangeActive"
         const val COL_CDMA_LEVEL = "cdmaLevel"
         const val COL_EMERGENCY = "emergencyOnly"
+        const val COL_IS_NTN = "isNtn"
         const val COL_IS_GSM = "isGsm"
         const val COL_IS_IN_SERVICE = "isInService"
         const val COL_OPERATOR = "operatorName"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index f44401b..77fd6be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -227,6 +227,12 @@
             .map { Utils.isInService(it.serviceState) }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
+    override val isNonTerrestrial =
+        callbackEvents
+            .mapNotNull { it.onServiceStateChanged }
+            .map { it.serviceState.isUsingNonTerrestrialNetwork }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
     override val isGsm =
         callbackEvents
             .mapNotNull { it.onSignalStrengthChanged }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index de46a5e..0e67756 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -19,11 +19,18 @@
 import android.os.OutcomeReceiver
 import android.telephony.satellite.NtnSignalStrengthCallback
 import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
 import android.telephony.satellite.SatelliteModemStateCallback
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.MessageInitializer
+import com.android.systemui.log.core.MessagePrinter
+import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
@@ -62,8 +69,10 @@
 /**
  * "Supported" here means supported by the device. The value of this should be stable during the
  * process lifetime.
+ *
+ * @VisibleForTesting
  */
-private sealed interface SatelliteSupport {
+sealed interface SatelliteSupport {
     /** Not yet fetched */
     data object Unknown : SatelliteSupport
 
@@ -123,6 +132,7 @@
     satelliteManagerOpt: Optional<SatelliteManager>,
     @Background private val bgDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
+    @OemSatelliteInputLog private val logBuffer: LogBuffer,
     private val systemClock: SystemClock,
 ) : DeviceBasedSatelliteRepository {
 
@@ -132,7 +142,8 @@
 
     // Some calls into satellite manager will throw exceptions if it is not supported.
     // This is never expected to change after boot, but may need to be retried in some cases
-    private val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown)
+    @get:VisibleForTesting
+    val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown)
 
     init {
         satelliteManager = satelliteManagerOpt.getOrNull()
@@ -145,6 +156,11 @@
                 ensureMinUptime(systemClock, MIN_UPTIME)
                 satelliteSupport.value = satelliteManager.checkSatelliteSupported()
 
+                logBuffer.i(
+                    { str1 = satelliteSupport.value.toString() },
+                    { "Checked for system support. support=$str1" },
+                )
+
                 // We only need to check location availability if this mode is supported
                 if (satelliteSupport.value is Supported) {
                     isSatelliteAllowedForCurrentLocation.subscriptionCount
@@ -159,6 +175,9 @@
                                  * connection might cause more frequent checks.
                                  */
                                 while (true) {
+                                    logBuffer.i {
+                                        "requestIsSatelliteCommunicationAllowedForCurrentLocation"
+                                    }
                                     checkIsSatelliteAllowed()
                                     delay(POLLING_INTERVAL_MS)
                                 }
@@ -167,6 +186,8 @@
                 }
             }
         } else {
+            logBuffer.i { "Satellite manager is null" }
+
             satelliteSupport.value = NotSupported
         }
     }
@@ -181,12 +202,21 @@
     private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
         conflatedCallbackFlow {
                 val cb = SatelliteModemStateCallback { state ->
+                    logBuffer.i({ int1 = state }) { "onSatelliteModemStateChanged: state=$int1" }
                     trySend(SatelliteConnectionState.fromModemState(state))
                 }
 
-                sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+                var registered = false
 
-                awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) }
+                try {
+                    val res =
+                        sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+                    registered = res == SATELLITE_RESULT_SUCCESS
+                } catch (e: Exception) {
+                    logBuffer.e("error registering for modem state", e)
+                }
+
+                awaitClose { if (registered) sm.unregisterForSatelliteModemStateChanged(cb) }
             }
             .flowOn(bgDispatcher)
 
@@ -197,12 +227,21 @@
     private fun signalStrengthFlow(sm: SupportedSatelliteManager) =
         conflatedCallbackFlow {
                 val cb = NtnSignalStrengthCallback { signalStrength ->
+                    logBuffer.i({ int1 = signalStrength.level }) {
+                        "onNtnSignalStrengthChanged: level=$int1"
+                    }
                     trySend(signalStrength.level)
                 }
 
-                sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+                var registered = false
+                try {
+                    sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+                    registered = true
+                } catch (e: Exception) {
+                    logBuffer.e("error registering for signal strength", e)
+                }
 
-                awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) }
+                awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) }
             }
             .flowOn(bgDispatcher)
 
@@ -213,11 +252,15 @@
                 bgDispatcher.asExecutor(),
                 object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
                     override fun onError(e: SatelliteManager.SatelliteException) {
-                        android.util.Log.e(TAG, "Found exception when checking for satellite: ", e)
+                        logBuffer.e(
+                            "Found exception when checking availability",
+                            e,
+                        )
                         isSatelliteAllowedForCurrentLocation.value = false
                     }
 
                     override fun onResult(allowed: Boolean) {
+                        logBuffer.i { allowed.toString() }
                         isSatelliteAllowedForCurrentLocation.value = allowed
                     }
                 }
@@ -239,12 +282,27 @@
                     }
 
                     override fun onError(error: SatelliteManager.SatelliteException) {
+                        logBuffer.e(
+                            "Exception when checking for satellite support. " +
+                                "Assuming it is not supported for this device.",
+                            error,
+                        )
+
                         // Assume that an error means it's not supported
                         continuation.resume(NotSupported)
                     }
                 }
 
-            requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb)
+            try {
+                requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb)
+            } catch (error: Exception) {
+                logBuffer.e(
+                    "Exception when checking for satellite support. " +
+                        "Assuming it is not supported for this device.",
+                    error,
+                )
+                continuation.resume(NotSupported)
+            }
         }
 
     companion object {
@@ -264,5 +322,19 @@
                 delay(timeTilMinUptime)
             }
         }
+
+        /** A couple of convenience logging methods rather than a whole class */
+        private fun LogBuffer.i(
+            initializer: MessageInitializer = {},
+            printer: MessagePrinter,
+        ) = this.log(TAG, LogLevel.INFO, initializer, printer)
+
+        private fun LogBuffer.e(message: String, exception: Throwable? = null) =
+            this.log(
+                tag = TAG,
+                level = LogLevel.ERROR,
+                message = message,
+                exception = exception,
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt
new file mode 100644
index 0000000..f5d0f6b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui
+
+import android.content.Context
+import com.android.internal.telephony.flags.Flags.oemEnabledSatelliteFlag
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
+import com.android.systemui.statusbar.pipeline.satellite.ui.binder.DeviceBasedSatelliteIconBinder
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+import javax.inject.Inject
+
+@SysUISingleton
+class DeviceBasedSatelliteBindableIcon
+@Inject
+constructor(
+    context: Context,
+    viewModel: DeviceBasedSatelliteViewModel,
+) : BindableIcon {
+    override val slot: String =
+        context.getString(com.android.internal.R.string.status_bar_oem_satellite)
+
+    override val initializer = ModernStatusBarViewCreator { context ->
+        SingleBindableStatusBarIconView.createView(context).also { view ->
+            view.initView(slot) { DeviceBasedSatelliteIconBinder.bind(view, viewModel) }
+        }
+    }
+
+    override val shouldBindIcon: Boolean = oemEnabledSatelliteFlag()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt
new file mode 100644
index 0000000..59ac5f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.binder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+import kotlinx.coroutines.launch
+
+object DeviceBasedSatelliteIconBinder {
+    fun bind(
+        view: SingleBindableStatusBarIconView,
+        viewModel: DeviceBasedSatelliteViewModel,
+    ): ModernStatusBarViewBinding {
+        return SingleBindableStatusBarIconView.withDefaultBinding(
+            view = view,
+            shouldBeVisible = { viewModel.icon.value != null }
+        ) {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.icon.collect { newIcon ->
+                        if (newIcon == null) {
+                            view.iconView.setImageDrawable(null)
+                        } else {
+                            IconViewBinder.bind(newIcon, view.iconView)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
new file mode 100644
index 0000000..6938d66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.model
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+
+/**
+ * Define the [Icon] that relates to a given satellite connection state + level. Note that for now
+ * We don't need any data class box, so we can just use a simple mapping function.
+ */
+object SatelliteIconModel {
+    fun fromConnectionState(
+        connectionState: SatelliteConnectionState,
+        signalStrength: Int,
+    ): Icon? =
+        when (connectionState) {
+            // TODO(b/316635648): check if this should be null
+            SatelliteConnectionState.Unknown,
+            SatelliteConnectionState.Off,
+            SatelliteConnectionState.On ->
+                Icon.Resource(
+                    res = R.drawable.ic_satellite_not_connected,
+                    contentDescription = null,
+                )
+            SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength)
+        }
+
+    private fun fromSignalStrength(
+        signalStrength: Int,
+    ): Icon? =
+        // TODO(b/316634365): these need content descriptions
+        when (signalStrength) {
+            // No signal
+            0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null)
+
+            // Poor -> Moderate
+            1,
+            2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null)
+
+            // Good -> Great
+            3,
+            4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null)
+            else -> null
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
new file mode 100644
index 0000000..0051161
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * View-Model for the device-based satellite icon. This icon will only show in the status bar if
+ * satellite is available AND all other service states are considered OOS.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class DeviceBasedSatelliteViewModel
+@Inject
+constructor(
+    interactor: DeviceBasedSatelliteInteractor,
+    @Application scope: CoroutineScope,
+) {
+    private val shouldShowIcon: StateFlow<Boolean> =
+        interactor.areAllConnectionsOutOfService
+            .flatMapLatest { allOos ->
+                if (!allOos) {
+                    flowOf(false)
+                } else {
+                    interactor.isSatelliteAllowed
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    val icon: StateFlow<Icon?> =
+        combine(
+                shouldShowIcon,
+                interactor.connectionState,
+                interactor.signalStrength,
+            ) { shouldShow, state, signalStrength ->
+                if (shouldShow) {
+                    SatelliteIconModel.fromConnectionState(state, signalStrength)
+                } else {
+                    null
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
index 3b87bed..25a2c9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
@@ -103,7 +103,7 @@
      *
      * Creates a dot view, and uses [bindingCreator] to get and set the binding.
      */
-    fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
+    open fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
         // The dot view requires [slot] to be set, and the [binding] may require an instantiated dot
         // view. So, this is the required order.
         this.slot = slot
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt
new file mode 100644
index 0000000..c663c37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.view
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+/** Simple single-icon view that is bound to bindable_status_bar_icon.xml */
+class SingleBindableStatusBarIconView(
+    context: Context,
+    attrs: AttributeSet?,
+) : ModernStatusBarView(context, attrs) {
+
+    internal lateinit var iconView: ImageView
+    internal lateinit var dotView: StatusBarIconView
+
+    override fun toString(): String {
+        return "SingleBindableStatusBarIcon(" +
+            "slot='$slot', " +
+            "isCollecting=${binding.isCollecting()}, " +
+            "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " +
+            "viewString=${super.toString()}"
+    }
+
+    override fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
+        super.initView(slot, bindingCreator)
+
+        iconView = requireViewById(R.id.icon_view)
+        dotView = requireViewById(R.id.status_bar_dot)
+    }
+
+    companion object {
+        fun createView(
+            context: Context,
+        ): SingleBindableStatusBarIconView {
+            return LayoutInflater.from(context).inflate(R.layout.bindable_status_bar_icon, null)
+                as SingleBindableStatusBarIconView
+        }
+
+        /**
+         * Using a given binding [block], create the necessary scaffolding to handle the general
+         * case of a single status bar icon. This includes eliding into a dot view when there is not
+         * enough space, and handling tint.
+         *
+         * [block] should be a simple [launch] call that handles updating the single icon view with
+         * its new view. Currently there is no simple way to e.g., extend to handle multiple tints
+         * for dual-layered icons, and any more complex logic should probably find a way to return
+         * its own version of [ModernStatusBarViewBinding].
+         */
+        fun withDefaultBinding(
+            view: SingleBindableStatusBarIconView,
+            shouldBeVisible: () -> Boolean,
+            block: suspend LifecycleOwner.(View) -> Unit
+        ): SingleBindableStatusBarIconViewBinding {
+            @StatusBarIconView.VisibleState
+            val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
+
+            val iconTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE)
+            val decorTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE)
+
+            var isCollecting: Boolean = false
+
+            view.repeatWhenAttached {
+                // Child binding
+                block(view)
+
+                lifecycleScope.launch {
+                    repeatOnLifecycle(Lifecycle.State.STARTED) {
+                        // isVisible controls the visibility state of the outer group, and thus it
+                        // needs
+                        // to run in the CREATED lifecycle so it can continue to watch while
+                        // invisible
+                        // See (b/291031862) for details
+                        launch {
+                            visibilityState.collect { visibilityState ->
+                                // for b/296864006, we can not hide all the child views if
+                                // visibilityState is STATE_HIDDEN. Because hiding all child views
+                                // would cause the
+                                // getWidth() of this view return 0, and that would cause the
+                                // translation
+                                // calculation fails in StatusIconContainer. Therefore, like class
+                                // MobileIconBinder, instead of set the child views visibility to
+                                // View.GONE,
+                                // we set their visibility to View.INVISIBLE to make them invisible
+                                // but
+                                // keep the width.
+                                ModernStatusBarViewVisibilityHelper.setVisibilityState(
+                                    visibilityState,
+                                    view.iconView,
+                                    view.dotView,
+                                )
+                            }
+                        }
+
+                        launch {
+                            iconTint.collect { tint ->
+                                val tintList = ColorStateList.valueOf(tint)
+                                view.iconView.imageTintList = tintList
+                                view.dotView.setDecorColor(tint)
+                            }
+                        }
+
+                        launch {
+                            decorTint.collect { decorTint -> view.dotView.setDecorColor(decorTint) }
+                        }
+
+                        try {
+                            awaitCancellation()
+                        } finally {
+                            isCollecting = false
+                        }
+                    }
+                }
+            }
+
+            return object : SingleBindableStatusBarIconViewBinding {
+                override val decorTint: Int
+                    get() = decorTint.value
+
+                override val iconTint: Int
+                    get() = iconTint.value
+
+                override fun getShouldIconBeVisible(): Boolean {
+                    return shouldBeVisible()
+                }
+
+                override fun onVisibilityStateChanged(state: Int) {
+                    visibilityState.value = state
+                }
+
+                override fun onIconTintChanged(newTint: Int, contrastTint: Int) {
+                    iconTint.value = newTint
+                }
+
+                override fun onDecorTintChanged(newTint: Int) {
+                    decorTint.value = newTint
+                }
+
+                override fun isCollecting(): Boolean {
+                    return isCollecting
+                }
+            }
+        }
+    }
+}
+
+@VisibleForTesting
+interface SingleBindableStatusBarIconViewBinding : ModernStatusBarViewBinding {
+    val iconTint: Int
+    val decorTint: Int
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 1528c9b..1414150 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -159,7 +159,7 @@
     public void showNotification(@NonNull NotificationEntry entry) {
         mLogger.logShowNotification(entry);
         addEntry(entry);
-        updateNotification(entry.getKey(), true /* show */);
+        updateNotification(entry.getKey(), true /* shouldHeadsUpAgain */);
         entry.setInterruption();
     }
 
@@ -190,12 +190,12 @@
     /**
      * Called when the notification state has been updated.
      * @param key the key of the entry that was updated
-     * @param show whether the notification should show again and force reevaluation of
-     *              removal time
+     * @param shouldHeadsUpAgain whether the notification should show again and force reevaluation
+     *                           of removal time
      */
-    public void updateNotification(@NonNull String key, boolean show) {
+    public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) {
         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
-        mLogger.logUpdateNotification(key, show, headsUpEntry != null);
+        mLogger.logUpdateNotification(key, shouldHeadsUpAgain, headsUpEntry != null);
         if (headsUpEntry == null) {
             // the entry was released before this update (i.e by a listener) This can happen
             // with the groupmanager
@@ -204,7 +204,7 @@
 
         headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
 
-        if (show) {
+        if (shouldHeadsUpAgain) {
             headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification");
             if (headsUpEntry != null) {
                 setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
index b8c7e20..a7352be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
@@ -182,7 +182,7 @@
      */
     fun unpinAll(userUnPinned: Boolean)
 
-    fun updateNotification(key: String, alert: Boolean)
+    fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
 }
 
 /** Sets the animation state of the HeadsUpManager. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java
new file mode 100644
index 0000000..970cc75
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+/**
+ * A controller which provides the current sensitive notification protections status as well as
+ * to assist in feature usage and exemptions
+ */
+public interface SensitiveNotificationProtectionController {
+    /**
+     * Register a runnable that triggers on changes to protection state
+     *
+     * <p> onSensitiveStateChanged not invoked on registration
+     */
+    void registerSensitiveStateListener(Runnable onSensitiveStateChanged);
+
+    /** Unregister a previously registered onSensitiveStateChanged runnable */
+    void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged);
+
+    /** Return {@code true} if device in state in which notifications should be protected */
+    boolean isSensitiveStateActive();
+
+    /** Return {@code true} when notification should be protected */
+    boolean shouldProtectNotification(NotificationEntry entry);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
new file mode 100644
index 0000000..3c4ca44
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import static com.android.systemui.Flags.screenshareNotificationHiding;
+
+import android.media.projection.MediaProjectionInfo;
+import android.media.projection.MediaProjectionManager;
+import android.os.Handler;
+import android.os.Trace;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.util.ListenerSet;
+
+import javax.inject.Inject;
+
+/** Implementation of SensitiveNotificationProtectionController. **/
+@SysUISingleton
+public class SensitiveNotificationProtectionControllerImpl
+        implements SensitiveNotificationProtectionController {
+    private final MediaProjectionManager mMediaProjectionManager;
+    private final ListenerSet<Runnable> mListeners = new ListenerSet<>();
+    private volatile MediaProjectionInfo mProjection;
+
+    @VisibleForTesting
+    final MediaProjectionManager.Callback mMediaProjectionCallback =
+            new MediaProjectionManager.Callback() {
+                @Override
+                public void onStart(MediaProjectionInfo info) {
+                    Trace.beginSection(
+                            "SNPC.onProjectionStart");
+                    mProjection = info;
+                    mListeners.forEach(Runnable::run);
+                    Trace.endSection();
+                }
+
+                @Override
+                public void onStop(MediaProjectionInfo info) {
+                    Trace.beginSection(
+                            "SNPC.onProjectionStop");
+                    mProjection = null;
+                    mListeners.forEach(Runnable::run);
+                    Trace.endSection();
+                }
+            };
+
+    @Inject
+    public SensitiveNotificationProtectionControllerImpl(
+            MediaProjectionManager mediaProjectionManager,
+            @Main Handler mainHandler) {
+        mMediaProjectionManager = mediaProjectionManager;
+
+        if (screenshareNotificationHiding()) {
+            mMediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler);
+        }
+    }
+
+    @Override
+    public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) {
+        mListeners.addIfAbsent(onSensitiveStateChanged);
+    }
+
+    @Override
+    public void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged) {
+        mListeners.remove(onSensitiveStateChanged);
+    }
+
+    @Override
+    public boolean isSensitiveStateActive() {
+        // TODO(b/316955558): Add disabled by developer option
+        // TODO(b/316955306): Add feature exemption for sysui and bug handlers
+        // TODO(b/316955346): Add feature exemption for single app screen sharing
+        return mProjection != null;
+    }
+
+    @Override
+    public boolean shouldProtectNotification(NotificationEntry entry) {
+        if (!isSensitiveStateActive()) {
+            return false;
+        }
+
+        // Exempt foreground service notifications from protection in effort to keep screen share
+        // stop actions easily accessible
+        // TODO(b/316955208): Exempt FGS notifications only for app that started projection
+        return !entry.getSbn().getNotification().isFgsOrUij();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index 3304b98..15200bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -60,6 +60,8 @@
 import com.android.systemui.statusbar.policy.RotationLockControllerImpl;
 import com.android.systemui.statusbar.policy.SecurityController;
 import com.android.systemui.statusbar.policy.SecurityControllerImpl;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionControllerImpl;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
 import com.android.systemui.statusbar.policy.SplitShadeStateControllerImpl;
 import com.android.systemui.statusbar.policy.UserInfoController;
@@ -146,6 +148,11 @@
 
     /** */
     @Binds
+    SensitiveNotificationProtectionController provideSensitiveNotificationProtectionController(
+            SensitiveNotificationProtectionControllerImpl controllerImpl);
+
+    /** */
+    @Binds
     UserInfoController provideUserInfoContrller(UserInfoControllerImpl controllerImpl);
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt
index ce811e2..10137a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt
@@ -17,10 +17,16 @@
 package com.android.systemui.statusbar.ui
 
 import com.android.internal.policy.SystemBarUtils
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.onConfigChanged
 import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 
@@ -31,6 +37,8 @@
 class SystemBarUtilsState
 @Inject
 constructor(
+    @Background bgContext: CoroutineContext,
+    @Main mainContext: CoroutineContext,
     configurationController: ConfigurationController,
     proxy: SystemBarUtilsProxy,
 ) {
@@ -38,5 +46,10 @@
     val statusBarHeight: Flow<Int> =
         configurationController.onConfigChanged
             .onStart<Any> { emit(Unit) }
+            .flowOn(mainContext)
+            .conflate()
             .map { proxy.getStatusBarHeight() }
+            .distinctUntilChanged()
+            .flowOn(bgContext)
+            .conflate()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java
index 972895d..039109e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java
+++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java
@@ -19,7 +19,11 @@
 import android.content.Context;
 import android.os.Handler;
 
-import javax.inject.Inject;
+import com.android.systemui.dagger.qualifiers.Background;
+
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
 
 /**
  * A wake lock that has a built in delay when releasing to give the framebuffer time to update.
@@ -32,9 +36,11 @@
     private final Handler mHandler;
     private final WakeLock mInner;
 
-    public DelayedWakeLock(Handler h, WakeLock inner) {
-        mHandler = h;
-        mInner = inner;
+    @AssistedInject
+    public DelayedWakeLock(@Background Handler handler, Context context, WakeLockLogger logger,
+            @Assisted String tag) {
+        mInner = WakeLock.createPartial(context, logger, tag);
+        mHandler = handler;
     }
 
     @Override
@@ -58,46 +64,11 @@
     }
 
     /**
-     * An injectable builder for {@see DelayedWakeLock} that has the context already filled in.
+     * Factory to create the instance of DelayedWakeLock class.
      */
-    public static class Builder {
-        private final Context mContext;
-        private final WakeLockLogger mLogger;
-        private String mTag;
-        private Handler mHandler;
-
-        /**
-         * Constructor for DelayedWakeLock.Builder
-         */
-        @Inject
-        public Builder(Context context, WakeLockLogger logger) {
-            mContext = context;
-            mLogger = logger;
-        }
-
-        /**
-         * Set the tag for the WakeLock.
-         */
-        public Builder setTag(String tag) {
-            mTag = tag;
-
-            return this;
-        }
-
-        /**
-         * Set the handler for the DelayedWakeLock.
-         */
-        public Builder setHandler(Handler handler) {
-            mHandler = handler;
-
-            return this;
-        }
-
-        /**
-         * Build the DelayedWakeLock.
-         */
-        public DelayedWakeLock build() {
-            return new DelayedWakeLock(mHandler, WakeLock.createPartial(mContext, mLogger, mTag));
-        }
+    @AssistedFactory
+    public interface Factory {
+        /** creates the instance of DelayedWakeLock class. */
+        DelayedWakeLock create(String tag);
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 9ee3d22..aee441a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -535,6 +535,7 @@
         }
         if (changed && fromKey) {
             Events.writeEvent(Events.EVENT_KEY, stream, lastAudibleStreamVolume);
+            mCallbacks.onVolumeChangedFromKey();
         }
         return changed;
     }
@@ -1030,6 +1031,18 @@
         }
 
         @Override
+        public void onVolumeChangedFromKey() {
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onVolumeChangedFromKey();
+                    }
+                });
+            }
+        }
+
+        @Override
         public void onAccessibilityModeChanged(Boolean showA11yStream) {
             boolean show = showA11yStream != null && showA11yStream;
             for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 404621d..b127c51 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -34,6 +34,7 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
 import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
+import static com.android.systemui.Flags.hapticVolumeSlider;
 import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
 
@@ -117,7 +118,11 @@
 import com.android.settingslib.Utils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.Prefs;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
+import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -125,6 +130,7 @@
 import com.android.systemui.plugins.VolumeDialogController.State;
 import com.android.systemui.plugins.VolumeDialogController.StreamState;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DevicePostureController;
@@ -140,6 +146,9 @@
 import java.util.List;
 import java.util.function.Consumer;
 
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.CoroutineScope;
+
 /**
  * Visual presentation of the volume dialog.
  *
@@ -303,6 +312,10 @@
     private int mOrientation;
     private final Lazy<SecureSettings> mSecureSettings;
     private int mDialogTimeoutMillis;
+    private final CoroutineDispatcher mMainDispatcher;
+    private final CoroutineScope mApplicationScope;
+    private final VibratorHelper mVibratorHelper;
+    private final com.android.systemui.util.time.SystemClock mSystemClock;
 
     public VolumeDialogImpl(
             Context context,
@@ -319,11 +332,18 @@
             DevicePostureController devicePostureController,
             Looper looper,
             DumpManager dumpManager,
-            Lazy<SecureSettings> secureSettings) {
+            Lazy<SecureSettings> secureSettings,
+            VibratorHelper vibratorHelper,
+            @Main CoroutineDispatcher mainDispatcher,
+            @Application CoroutineScope applicationScope,
+            com.android.systemui.util.time.SystemClock systemClock) {
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
         mHandler = new H(looper);
-
+        mMainDispatcher = mainDispatcher;
+        mApplicationScope = applicationScope;
+        mVibratorHelper = vibratorHelper;
+        mSystemClock = systemClock;
         mShouldListenForJank = shouldListenForJank;
         mController = volumeDialogController;
         mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
@@ -839,6 +859,7 @@
             row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)});
         }
         row.slider = row.view.findViewById(R.id.volume_row_slider);
+        row.createPlugin(mVibratorHelper, mSystemClock, mMainDispatcher, mApplicationScope);
         row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row));
         row.number = row.view.findViewById(R.id.volume_number);
 
@@ -1480,6 +1501,12 @@
         mController.getCaptionsComponentState(false);
         checkODICaptionsTooltip(false);
         updateBackgroundForDrawerClosedAmount();
+        for (int i = 0; i < mRows.size(); i++) {
+            VolumeRow row = mRows.get(i);
+            if (row.slider.getVisibility() == VISIBLE) {
+                row.addHaptics();
+            }
+        }
         Trace.endSection();
     }
 
@@ -1532,7 +1559,9 @@
 
     protected void dismissH(int reason) {
         Trace.beginSection("VolumeDialogImpl#dismissH");
-
+        for (int i = 0; i < mRows.size(); i++) {
+            mRows.get(i).removeHaptics();
+        }
         Log.i(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason]
                 + " from: " + Debug.getCaller());
 
@@ -2358,6 +2387,14 @@
         public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkForSwitchState) {
             updateCaptionsEnabledH(isEnabled, checkForSwitchState);
         }
+
+        @Override
+        public void onVolumeChangedFromKey() {
+            VolumeRow activeRow = getActiveRow();
+            if (activeRow.mHapticPlugin != null) {
+                activeRow.mHapticPlugin.onKeyDown();
+            }
+        }
     };
 
     @VisibleForTesting void onPostureChanged(int posture) {
@@ -2459,6 +2496,15 @@
         @Override
         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
             if (mRow.ss == null) return;
+            if (getActiveRow().equals(mRow)
+                    && mRow.slider.getVisibility() == VISIBLE
+                    && mRow.mHapticPlugin != null) {
+                mRow.mHapticPlugin.onProgressChanged(seekBar, progress, fromUser);
+                if (!fromUser) {
+                    // Consider a change from program as the volume key being continuously pressed
+                    mRow.mHapticPlugin.onKeyDown();
+                }
+            }
             if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream)
                     + " onProgressChanged " + progress + " fromUser=" + fromUser);
             if (!fromUser) return;
@@ -2485,6 +2531,9 @@
         @Override
         public void onStartTrackingTouch(SeekBar seekBar) {
             if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream);
+            if (mRow.mHapticPlugin != null) {
+                mRow.mHapticPlugin.onStartTrackingTouch(seekBar);
+            }
             mController.setActiveStream(mRow.stream);
             mRow.tracking = true;
         }
@@ -2492,6 +2541,9 @@
         @Override
         public void onStopTrackingTouch(SeekBar seekBar) {
             if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream);
+            if (mRow.mHapticPlugin != null) {
+                mRow.mHapticPlugin.onStopTrackingTouch(seekBar);
+            }
             mRow.tracking = false;
             mRow.userAttempt = SystemClock.uptimeMillis();
             final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress());
@@ -2524,6 +2576,22 @@
     }
 
     private static class VolumeRow {
+        private static final SliderHapticFeedbackConfig sSliderHapticFeedbackConfig =
+                new SliderHapticFeedbackConfig(
+                /* velocityInterpolatorFactor= */ 1f,
+                /* progressInterpolatorFactor= */ 1f,
+                /* progressBasedDragMinScale= */ 0f,
+                /* progressBasedDragMaxScale= */ 0.2f,
+                /* additionalVelocityMaxBump= */ 0.15f,
+                /* deltaMillisForDragInterval= */ 0f,
+                /* deltaProgressForDragThreshold= */ 0.015f,
+                /* numberOfLowTicks= */ 5,
+                /* maxVelocityToScale= */ 300f,
+                /* velocityAxis= */ MotionEvent.AXIS_Y,
+                /* upperBookendScale= */ 1f,
+                /* lowerBookendScale= */ 0.05f,
+                /* exponent= */ 1f / 0.89f);
+
         private View view;
         private TextView header;
         private ImageButton icon;
@@ -2544,6 +2612,7 @@
         private ObjectAnimator anim;  // slider progress animation for non-touch-related updates
         private int animTargetProgress;
         private int lastAudibleLevel = 1;
+        private SeekableSliderHapticPlugin mHapticPlugin;
 
         void setIcon(int iconRes, Resources.Theme theme) {
             if (icon != null) {
@@ -2554,6 +2623,50 @@
                 sliderProgressIcon.setDrawable(view.getResources().getDrawable(iconRes, theme));
             }
         }
+
+        void createPlugin(
+                VibratorHelper vibratorHelper,
+                com.android.systemui.util.time.SystemClock systemClock,
+                CoroutineDispatcher mainDispatcher,
+                CoroutineScope applicationScope) {
+            if (!hapticVolumeSlider() || mHapticPlugin != null) return;
+
+            mHapticPlugin = new SeekableSliderHapticPlugin(
+                    vibratorHelper,
+                    systemClock,
+                    mainDispatcher,
+                    applicationScope,
+                    sSliderHapticFeedbackConfig);
+        }
+
+
+        @SuppressLint("ClickableViewAccessibility")
+        void addTouchListener() {
+            slider.setOnTouchListener(new View.OnTouchListener() {
+                @Override
+                public boolean onTouch(View view, MotionEvent motionEvent) {
+                    if (mHapticPlugin != null) {
+                        mHapticPlugin.onTouchEvent(motionEvent);
+                    }
+                    return false;
+                }
+            });
+        }
+
+        void addHaptics() {
+            if (mHapticPlugin != null) {
+                addTouchListener();
+                mHapticPlugin.start();
+            }
+        }
+
+        @SuppressLint("ClickableViewAccessibility")
+        void removeHaptics() {
+            slider.setOnTouchListener(null);
+            if (mHapticPlugin != null) {
+                mHapticPlugin.stop();
+            }
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 497c4cb..f180a94 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -22,16 +22,20 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.CoreStartable;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
 import com.android.systemui.plugins.VolumeDialogController;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.time.SystemClock;
 import com.android.systemui.volume.CsdWarningDialog;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.volume.VolumeDialogComponent;
@@ -49,6 +53,9 @@
 import dagger.multibindings.IntoMap;
 import dagger.multibindings.IntoSet;
 
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.CoroutineScope;
+
 /** Dagger Module for code in the volume package. */
 @Module(
         subcomponents = {
@@ -90,7 +97,11 @@
             CsdWarningDialog.Factory csdFactory,
             DevicePostureController devicePostureController,
             DumpManager dumpManager,
-            Lazy<SecureSettings> secureSettings) {
+            Lazy<SecureSettings> secureSettings,
+            VibratorHelper vibratorHelper,
+            @Main CoroutineDispatcher mainDispatcher,
+            @Application CoroutineScope applicationScope,
+            SystemClock systemClock) {
         VolumeDialogImpl impl = new VolumeDialogImpl(
                 context,
                 volumeDialogController,
@@ -106,7 +117,11 @@
                 devicePostureController,
                 Looper.getMainLooper(),
                 dumpManager,
-                secureSettings);
+                secureSettings,
+                vibratorHelper,
+                mainDispatcher,
+                applicationScope,
+                systemClock);
         impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
         impl.setAutomute(true);
         impl.setSilentMode(false);
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
index b8f9583..1ba269e 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
@@ -24,7 +24,7 @@
 import android.os.UserHandle
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.user.data.model.SelectedUserModel
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.repository.UserRepository
@@ -54,7 +54,7 @@
 class WallpaperRepositoryImpl
 @Inject
 constructor(
-    @Application scope: CoroutineScope,
+    @Background scope: CoroutineScope,
     broadcastDispatcher: BroadcastDispatcher,
     userRepository: UserRepository,
     private val wallpaperManager: WallpaperManager,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index 5e5273b..bc9a0a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -181,16 +181,16 @@
     @Test
     public void onAttachedToWindow_menuIsVisible() {
         mMenuViewLayer.onAttachedToWindow();
-        final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
 
+        final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
         assertThat(menuView.getVisibility()).isEqualTo(VISIBLE);
     }
 
     @Test
-    public void onAttachedToWindow_menuIsGone() {
+    public void onDetachedFromWindow_menuIsGone() {
         mMenuViewLayer.onDetachedFromWindow();
-        final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
 
+        final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
         assertThat(menuView.getVisibility()).isEqualTo(GONE);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
new file mode 100644
index 0000000..df73cc8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.stickykeys.ui
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.keyboard.data.repository.FakeStickyKeysRepository
+import com.android.systemui.keyboard.data.repository.keyboardRepository
+import com.android.systemui.keyboard.stickykeys.StickyKeysLogger
+import com.android.systemui.keyboard.stickykeys.shared.model.Locked
+import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.ComponentSystemUIDialog
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class StickyKeysIndicatorCoordinatorTest : SysuiTestCase() {
+
+    private lateinit var coordinator: StickyKeysIndicatorCoordinator
+    private val testScope = TestScope(StandardTestDispatcher())
+    private val stickyKeysRepository = FakeStickyKeysRepository()
+    private val dialog = mock<ComponentSystemUIDialog>()
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(ComposeFacade.isComposeAvailable())
+        val dialogFactory = mock<SystemUIDialogFactory> {
+            whenever(applicationContext).thenReturn(context)
+            whenever(create(any(), anyInt(), anyBoolean())).thenReturn(dialog)
+        }
+        val keyboardRepository = Kosmos().keyboardRepository
+        val viewModel = StickyKeysIndicatorViewModel(
+                stickyKeysRepository,
+                keyboardRepository,
+                testScope.backgroundScope)
+        coordinator = StickyKeysIndicatorCoordinator(
+                testScope.backgroundScope,
+                dialogFactory,
+                viewModel,
+                mock<StickyKeysLogger>())
+        coordinator.startListening()
+        keyboardRepository.setIsAnyKeyboardConnected(true)
+    }
+
+    @Test
+    fun dialogIsShownWhenStickyKeysAreEmitted() {
+        testScope.run {
+            verifyZeroInteractions(dialog)
+
+            stickyKeysRepository.setStickyKeys(linkedMapOf(SHIFT to Locked(true)))
+            runCurrent()
+
+            verify(dialog).show()
+        }
+    }
+
+    @Test
+    fun dialogDisappearsWhenStickyKeysAreEmpty() {
+        testScope.run {
+            verifyZeroInteractions(dialog)
+
+            stickyKeysRepository.setStickyKeys(linkedMapOf(SHIFT to Locked(true)))
+            runCurrent()
+            stickyKeysRepository.setStickyKeys(linkedMapOf())
+            runCurrent()
+
+            verify(dialog).dismiss()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
index d397fc2..8a71368 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -46,6 +47,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class StickyKeysIndicatorViewModelTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index b57cf53..754a7fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -97,6 +97,7 @@
 import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.scene.FakeWindowRootViewComponent;
@@ -150,6 +151,7 @@
 @TestableLooper.RunWithLooper
 @SmallTest
 public class KeyguardViewMediatorTest extends SysuiTestCase {
+    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
     private KeyguardViewMediator mViewMediator;
 
     private final TestScope mTestScope = TestScopeProvider.getTestScope();
@@ -265,7 +267,8 @@
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
-                mSceneContainerFlags);
+                mSceneContainerFlags,
+                mKosmos::getCommunalInteractor);
         mFeatureFlags = new FakeFeatureFlags();
         mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false);
         mSetFlagsRule.enableFlags(FLAG_REFACTOR_GET_CURRENT_USER);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 90eaa5a..dafd9e6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -23,8 +23,9 @@
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.flags.FakeFeatureFlags
@@ -34,6 +35,8 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
@@ -42,11 +45,15 @@
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
 import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -57,7 +64,6 @@
 import kotlinx.coroutines.cancelChildren
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runCurrent
@@ -83,7 +89,8 @@
 @SmallTest
 @RunWith(JUnit4::class)
 class KeyguardTransitionScenariosTest : SysuiTestCase() {
-    private lateinit var testScope: TestScope
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
@@ -119,17 +126,14 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        val testDispatcher = StandardTestDispatcher()
-        testScope = TestScope(testDispatcher)
 
-        keyguardRepository = FakeKeyguardRepository()
-        bouncerRepository = FakeKeyguardBouncerRepository()
+        keyguardRepository = kosmos.fakeKeyguardRepository
+        bouncerRepository = kosmos.fakeKeyguardBouncerRepository
         commandQueue = FakeCommandQueue()
-        shadeRepository = FakeShadeRepository()
-        transitionRepository = spy(FakeKeyguardTransitionRepository())
+        shadeRepository = kosmos.fakeShadeRepository
+        transitionRepository = spy(kosmos.fakeKeyguardTransitionRepository)
         powerInteractor = PowerInteractorFactory.create().powerInteractor
-        communalInteractor =
-            CommunalInteractorFactory.create(testScope = testScope).communalInteractor
+        communalInteractor = kosmos.communalInteractor
 
         whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
 
@@ -160,8 +164,8 @@
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -184,8 +188,8 @@
         fromPrimaryBouncerTransitionInteractor =
             FromPrimaryBouncerTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -199,8 +203,8 @@
         fromDreamingTransitionInteractor =
             FromDreamingTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -210,8 +214,8 @@
         fromDreamingLockscreenHostedTransitionInteractor =
             FromDreamingLockscreenHostedTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -221,8 +225,8 @@
         fromAodTransitionInteractor =
             FromAodTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -232,8 +236,8 @@
         fromGoneTransitionInteractor =
             FromGoneTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -244,8 +248,8 @@
         fromDozingTransitionInteractor =
             FromDozingTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -257,8 +261,8 @@
         fromOccludedTransitionInteractor =
             FromOccludedTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -269,8 +273,8 @@
         fromAlternateBouncerTransitionInteractor =
             FromAlternateBouncerTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
@@ -281,8 +285,8 @@
         fromGlanceableHubTransitionInteractor =
             FromGlanceableHubTransitionInteractor(
                     scope = testScope,
-                    bgDispatcher = testDispatcher,
-                    mainDispatcher = testDispatcher,
+                    bgDispatcher = kosmos.testDispatcher,
+                    mainDispatcher = kosmos.testDispatcher,
                     glanceableHubTransitions = glanceableHubTransitions,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index 6be9275..ba7927d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -25,8 +25,7 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardViewController
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
 import com.android.systemui.dreams.DreamOverlayStateController
@@ -43,6 +42,7 @@
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
+import com.android.systemui.testKosmos
 import com.android.systemui.util.animation.UniqueObjectHostView
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -79,6 +79,8 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class MediaHierarchyManagerTest : SysuiTestCase() {
 
+    private val kosmos = testKosmos()
+
     @Mock private lateinit var lockHost: MediaHost
     @Mock private lateinit var qsHost: MediaHost
     @Mock private lateinit var qqsHost: MediaHost
@@ -110,10 +112,7 @@
     private lateinit var isQsBypassingShade: MutableStateFlow<Boolean>
     private lateinit var mediaFrame: ViewGroup
     private val configurationController = FakeConfigurationController()
-    private val communalRepository =
-        FakeCommunalRepository(applicationScope = testScope.backgroundScope)
-    private val communalInteractor =
-        CommunalInteractorFactory.create(communalRepository = communalRepository).communalInteractor
+    private val communalInteractor = kosmos.communalInteractor
     private val settings = FakeSettings()
     private lateinit var testableLooper: TestableLooper
     private lateinit var fakeHandler: FakeHandler
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index 0a464e6..b701d7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -21,6 +21,7 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
+import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.models.player.MediaViewHolder
@@ -171,6 +172,38 @@
     }
 
     @Test
+    fun testObtainViewState_expandedMatchesParentHeight() {
+        mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
+        player.measureState =
+            TransitionViewState().apply {
+                this.height = 100
+                this.measureHeight = 100
+            }
+        mediaHostStateHolder.expandedMatchesParentHeight = true
+        mediaHostStateHolder.expansion = 1f
+        mediaHostStateHolder.measurementInput =
+            MeasurementInput(
+                View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
+            )
+
+        // Assign the height of each expanded layout
+        MediaViewHolder.backgroundIds.forEach { id ->
+            mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 100
+        }
+
+        mediaViewController.obtainViewState(mediaHostStateHolder)
+
+        // Verify height of each expanded layout is updated to match constraint
+        MediaViewHolder.backgroundIds.forEach { id ->
+            assertTrue(
+                mediaViewController.expandedLayout.getConstraint(id).layout.mHeight ==
+                    ConstraintSet.MATCH_CONSTRAINT
+            )
+        }
+    }
+
+    @Test
     fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() {
         whenever(mockViewState.copy()).thenReturn(mockCopiedState)
         whenever(mockCopiedState.widgetStates)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 72847a6..c6cfabc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -414,6 +414,18 @@
 
     @Test
     public void onDeviceListUpdate_verifyDeviceListCallback() {
+        // This test relies on mMediaOutputController.start being called while the selected device
+        // list has exactly one item, and that item's id is:
+        // - Different from both ids in mMediaDevices.
+        // - Different from the id of the route published by the device under test (usually the
+        //   built-in speakers).
+        // So mock the selected device to respect these two preconditions.
+        MediaDevice mockSelectedMediaDevice = Mockito.mock(MediaDevice.class);
+        when(mockSelectedMediaDevice.getId()).thenReturn(TEST_DEVICE_3_ID);
+        doReturn(List.of(mockSelectedMediaDevice))
+                .when(mLocalMediaManager)
+                .getSelectedMediaDevice();
+
         mMediaOutputController.start(mCb);
         reset(mCb);
 
@@ -434,6 +446,18 @@
 
     @Test
     public void advanced_onDeviceListUpdateWithConnectedDeviceRemote_verifyItemSize() {
+        // This test relies on mMediaOutputController.start being called while the selected device
+        // list has exactly one item, and that item's id is:
+        // - Different from both ids in mMediaDevices.
+        // - Different from the id of the route published by the device under test (usually the
+        //   built-in speakers).
+        // So mock the selected device to respect these two preconditions.
+        MediaDevice mockSelectedMediaDevice = Mockito.mock(MediaDevice.class);
+        when(mockSelectedMediaDevice.getId()).thenReturn(TEST_DEVICE_3_ID);
+        doReturn(List.of(mockSelectedMediaDevice))
+                .when(mLocalMediaManager)
+                .getSelectedMediaDevice();
+
         when(mMediaDevice1.getFeatures()).thenReturn(
                 ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK));
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
index c8c134a..563a3fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
@@ -47,6 +48,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.FrameLayout;
 
 import androidx.lifecycle.Lifecycle;
 import androidx.test.filters.SmallTest;
@@ -63,6 +65,7 @@
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.CommandQueue;
@@ -111,7 +114,8 @@
     @Mock private FooterActionsViewBinder mFooterActionsViewBinder;
     @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
     @Mock private FeatureFlagsClassic mFeatureFlags;
-    private View mQsView;
+    @Mock private SceneContainerFlags mSceneContainerFlags;
+    private ViewGroup mQsView;
 
     private final CommandQueue mCommandQueue =
             new CommandQueue(mContext, new FakeDisplayTracker(mContext));
@@ -121,6 +125,9 @@
 
     @Before
     public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mSceneContainerFlags.isEnabled()).thenReturn(false);
+
         mUnderTest = instantiate();
 
         mUnderTest.onComponentCreated(mQsComponent, null);
@@ -487,9 +494,24 @@
         verify(mQSAnimator).setOnKeyguard(true);
     }
 
-    private QSImpl instantiate() {
-        MockitoAnnotations.initMocks(this);
+    @Test
+    public void testSceneContainerFlagsEnabled_FooterActionsRemoved_controllerNotStarted() {
+        when(mSceneContainerFlags.isEnabled()).thenReturn(true);
+        clearInvocations(
+                mFooterActionsViewBinder, mFooterActionsViewModel, mFooterActionsViewModelFactory);
+        QSImpl other = instantiate();
 
+        other.onComponentCreated(mQsComponent, null);
+
+        assertThat((View) other.getView().findViewById(R.id.qs_footer_actions)).isNull();
+        verifyZeroInteractions(
+                mFooterActionsViewModel,
+                mFooterActionsViewBinder,
+                mFooterActionsViewModelFactory
+        );
+    }
+
+    private QSImpl instantiate() {
         setupQsComponent();
         setUpViews();
         setUpInflater();
@@ -514,7 +536,8 @@
                 mFooterActionsViewModelFactory,
                 mFooterActionsViewBinder,
                 mLargeScreenShadeInterpolator,
-                mFeatureFlags);
+                mFeatureFlags,
+                mSceneContainerFlags);
     }
 
     private void setUpOther() {
@@ -533,14 +556,23 @@
     }
 
     private void setUpViews() {
-        mQsView = spy(new View(mContext));
+        mQsView = spy(new FrameLayout(mContext));
         when(mQsComponent.getRootView()).thenReturn(mQsView);
-        when(mQsView.findViewById(R.id.expanded_qs_scroll_view))
+
+        when(mQSPanelScrollView.findViewById(R.id.expanded_qs_scroll_view))
                 .thenReturn(mQSPanelScrollView);
-        when(mQsView.findViewById(R.id.header)).thenReturn(mHeader);
-        when(mQsView.findViewById(android.R.id.edit)).thenReturn(new View(mContext));
-        when(mQsView.findViewById(R.id.qs_footer_actions)).thenAnswer(
-                invocation -> new FooterActionsViewBinder().create(mContext));
+        mQsView.addView(mQSPanelScrollView);
+
+        when(mHeader.findViewById(R.id.header)).thenReturn(mHeader);
+        mQsView.addView(mHeader);
+
+        View customizer = new View(mContext);
+        customizer.setId(android.R.id.edit);
+        mQsView.addView(customizer);
+
+        View footerActionsView = new FooterActionsViewBinder().create(mContext);
+        footerActionsView.setId(R.id.qs_footer_actions);
+        mQsView.addView(footerActionsView);
     }
 
     private void setUpInflater() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index b7a9ea7..c1f5d85 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -25,14 +25,16 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.compose.ComposeFacade
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -52,6 +54,8 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 class GlanceableHubContainerControllerTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
     @Mock private lateinit var communalViewModel: CommunalViewModel
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock private lateinit var shadeInteractor: ShadeInteractor
@@ -71,9 +75,8 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        val withDeps = CommunalInteractorFactory.create()
-        communalInteractor = withDeps.communalInteractor
-        communalRepository = withDeps.communalRepository
+        communalInteractor = kosmos.communalInteractor
+        communalRepository = kosmos.fakeCommunalRepository
 
         underTest =
             GlanceableHubContainerController(
@@ -91,7 +94,8 @@
             .thenReturn(bouncerShowingFlow)
         whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow)
 
-        overrideResource(R.dimen.communal_grid_gutter_size, SWIPE_REGION_WIDTH)
+        overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH)
+        overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH)
     }
 
     @Test
@@ -132,7 +136,7 @@
         initAndAttachContainerView()
 
         // Touch events are intercepted.
-        assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue()
+        assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue()
     }
 
     @Test
@@ -144,7 +148,7 @@
 
         // Initial touch down is intercepted, and so are touches outside of the region, until an up
         // event is received.
-        assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue()
+        assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue()
         assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
         assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
         assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
@@ -165,6 +169,17 @@
     }
 
     @Test
+    fun onTouchEvent_topSwipeWhenHubOpen_returnsFalse() {
+        // Communal is open.
+        communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+        initAndAttachContainerView()
+
+        // Touch event in the top swipe reqgion is not intercepted.
+        assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
+    }
+
+    @Test
     fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() {
         // Communal is open.
         communalRepository.setDesiredScene(CommunalSceneKey.Communal)
@@ -210,11 +225,29 @@
     companion object {
         private const val CONTAINER_WIDTH = 100
         private const val CONTAINER_HEIGHT = 100
-        private const val SWIPE_REGION_WIDTH = 20
+        private const val RIGHT_SWIPE_REGION_WIDTH = 20
+        private const val TOP_SWIPE_REGION_WIDTH = 20
 
-        private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
-        private val DOWN_IN_SWIPE_REGION_EVENT =
+        private val DOWN_EVENT =
+            MotionEvent.obtain(
+                0L,
+                0L,
+                MotionEvent.ACTION_DOWN,
+                CONTAINER_WIDTH.toFloat(),
+                CONTAINER_HEIGHT.toFloat(),
+                0
+            )
+        private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT =
             MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0)
+        private val DOWN_IN_TOP_SWIPE_REGION_EVENT =
+            MotionEvent.obtain(
+                0L,
+                0L,
+                MotionEvent.ACTION_DOWN,
+                0f,
+                TOP_SWIPE_REGION_WIDTH.toFloat(),
+                0
+            )
         private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
         private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 11da237..200e758 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -57,7 +57,6 @@
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
 import com.android.systemui.communal.domain.interactor.CommunalInteractor;
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
@@ -202,8 +201,7 @@
                 new ConfigurationInteractor(configurationRepository),
                 shadeRepository,
                 () -> sceneInteractor);
-        CommunalInteractor communalInteractor =
-                CommunalInteractorFactory.create().getCommunalInteractor();
+        CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor();
 
         FakeKeyguardTransitionRepository keyguardTransitionRepository =
                 new FakeKeyguardTransitionRepository();
@@ -303,7 +301,8 @@
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
-                mSceneContainerFlags) {
+                mSceneContainerFlags,
+                () -> communalInteractor) {
                     @Override
                     protected boolean isDebuggable() {
                         return false;
@@ -451,6 +450,24 @@
     }
 
     @Test
+    public void setCommunalShowing_userTimeout() {
+        setKeyguardShowing();
+        clearInvocations(mWindowManager);
+
+        mNotificationShadeWindowController.onCommunalShowingChanged(true);
+        verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
+        assertThat(mLayoutParameters.getValue().userActivityTimeout)
+                .isEqualTo(CommunalInteractor.AWAKE_INTERVAL_MS);
+        clearInvocations(mWindowManager);
+
+        // Bouncer showing over communal overrides communal value
+        mNotificationShadeWindowController.setBouncerShowing(true);
+        verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
+        assertThat(mLayoutParameters.getValue().userActivityTimeout)
+                .isEqualTo(KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS);
+    }
+
+    @Test
     public void setKeyguardShowing_notFocusable_byDefault() {
         mNotificationShadeWindowController.setKeyguardShowing(false);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 8f46a37..6fc88ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -43,7 +43,6 @@
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
 import com.android.systemui.communal.domain.interactor.CommunalInteractor;
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
@@ -233,8 +232,7 @@
                 new ConfigurationInteractor(configurationRepository),
                 mShadeRepository,
                 () -> sceneInteractor);
-        CommunalInteractor communalInteractor =
-                CommunalInteractorFactory.create().getCommunalInteractor();
+        CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor();
 
         FakeKeyguardTransitionRepository keyguardTransitionRepository =
                 new FakeKeyguardTransitionRepository();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 05e866e..13934da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -27,7 +27,7 @@
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
@@ -144,7 +144,7 @@
                 { fromLockscreenTransitionInteractor },
                 { fromPrimaryBouncerTransitionInteractor }
             )
-        val communalInteractor = CommunalInteractorFactory.create().communalInteractor
+        val communalInteractor = kosmos.communalInteractor
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                 keyguardTransitionRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
index df547ae..350ed2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
@@ -17,9 +17,11 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
 import android.service.notification.StatusBarNotification
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -33,6 +35,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -55,28 +58,31 @@
     val statusBarStateController: StatusBarStateController = mock()
     val keyguardStateController: KeyguardStateController = mock()
     val mSelectedUserInteractor: SelectedUserInteractor = mock()
+    val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController =
+        mock()
 
     val coordinator: SensitiveContentCoordinator =
-        DaggerTestSensitiveContentCoordinatorComponent
-                .factory()
-                .create(
-                        dynamicPrivacyController,
-                        lockscreenUserManager,
-                        keyguardUpdateMonitor,
-                        statusBarStateController,
-                        keyguardStateController,
-                        mSelectedUserInteractor)
-                .coordinator
+        DaggerTestSensitiveContentCoordinatorComponent.factory()
+            .create(
+                dynamicPrivacyController,
+                lockscreenUserManager,
+                keyguardUpdateMonitor,
+                statusBarStateController,
+                keyguardStateController,
+                mSelectedUserInteractor,
+                sensitiveNotificationProtectionController
+            )
+            .coordinator
 
     @Test
     fun onDynamicPrivacyChanged_invokeInvalidationListener() {
         coordinator.attach(pipeline)
-        val invalidator = withArgCaptor<Invalidator> {
-            verify(pipeline).addPreRenderInvalidator(capture())
-        }
-        val dynamicPrivacyListener = withArgCaptor<DynamicPrivacyController.Listener> {
-            verify(dynamicPrivacyController).addListener(capture())
-        }
+        val invalidator =
+            withArgCaptor<Invalidator> { verify(pipeline).addPreRenderInvalidator(capture()) }
+        val dynamicPrivacyListener =
+            withArgCaptor<DynamicPrivacyController.Listener> {
+                verify(dynamicPrivacyController).addListener(capture())
+            }
 
         val invalidationListener = mock<Pluggable.PluggableListener<Invalidator>>()
         invalidator.setInvalidationListener(invalidationListener)
@@ -89,9 +95,10 @@
     @Test
     fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction() {
         coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
 
         whenever(lockscreenUserManager.currentUserId).thenReturn(1)
         whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
@@ -105,11 +112,59 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_sensitiveActive() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, false)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(false, true)
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_shouldProtectNotification() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, false)
+        whenever(
+                sensitiveNotificationProtectionController.shouldProtectNotification(
+                    entry.getRepresentativeEntry()
+                )
+            )
+            .thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, false)
+    }
+
+    @Test
     fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction() {
         coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
 
         whenever(lockscreenUserManager.currentUserId).thenReturn(1)
         whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
@@ -123,11 +178,59 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_sensitiveActive() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, true)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(false, true)
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_shouldProtectNotification() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, true)
+        whenever(
+                sensitiveNotificationProtectionController.shouldProtectNotification(
+                    entry.getRepresentativeEntry()
+                )
+            )
+            .thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, false)
+    }
+
+    @Test
     fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs() {
         coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
 
         whenever(lockscreenUserManager.currentUserId).thenReturn(1)
         whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -141,11 +244,59 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_sensitiveActive() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, false)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(false, true)
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_shouldProtectNotification() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, false)
+        whenever(
+                sensitiveNotificationProtectionController.shouldProtectNotification(
+                    entry.getRepresentativeEntry()
+                )
+            )
+            .thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, false)
+    }
+
+    @Test
     fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction() {
         coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
 
         whenever(lockscreenUserManager.currentUserId).thenReturn(1)
         whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -159,11 +310,61 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    @Suppress("ktlint:standard:max-line-length")
+    fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_sensitiveActive() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, false)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(false, true)
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    @Suppress("ktlint:standard:max-line-length")
+    fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_shouldProtectNotification() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, false)
+        whenever(
+                sensitiveNotificationProtectionController.shouldProtectNotification(
+                    entry.getRepresentativeEntry()
+                )
+            )
+            .thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, true)
+    }
+
+    @Test
     fun onBeforeRenderList_deviceLocked_notifNeedsRedaction() {
         coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
 
         whenever(lockscreenUserManager.currentUserId).thenReturn(1)
         whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -177,11 +378,59 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_sensitiveActive() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, true)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, true)
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_shouldProtectNotification() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+        val entry = fakeNotification(1, true)
+        whenever(
+                sensitiveNotificationProtectionController.shouldProtectNotification(
+                    entry.getRepresentativeEntry()
+                )
+            )
+            .thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, true)
+    }
+
+    @Test
     fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction() {
         coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
 
         whenever(lockscreenUserManager.currentUserId).thenReturn(1)
         whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -195,18 +444,66 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_sensitiveActive() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
+        val entry = fakeNotification(1, true)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(false, true)
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    @Suppress("ktlint:standard:max-line-length")
+    fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_shouldProtectNotification() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
+        val entry = fakeNotification(1, true)
+        whenever(
+                sensitiveNotificationProtectionController.shouldProtectNotification(
+                    entry.getRepresentativeEntry()
+                )
+            )
+            .thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, true)
+    }
+
+    @Test
     fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge() {
         coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
 
         whenever(lockscreenUserManager.currentUserId).thenReturn(1)
         whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
         whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
         whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
         whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true)
-
         val entry = fakeNotification(2, true)
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
@@ -215,11 +512,62 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_sensitiveActive() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
+        whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true)
+        val entry = fakeNotification(2, true)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, true)
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    @Suppress("ktlint:standard:max-line-length")
+    fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_shouldProtectNotification() {
+        coordinator.attach(pipeline)
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
+
+        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
+        whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true)
+        val entry = fakeNotification(2, true)
+        whenever(
+                sensitiveNotificationProtectionController.shouldProtectNotification(
+                    entry.getRepresentativeEntry()
+                )
+            )
+            .thenReturn(true)
+
+        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+        verify(entry.representativeEntry!!).setSensitive(true, true)
+    }
+
+    @Test
     fun onBeforeRenderList_deviceDynamicallyUnlocked_deviceBiometricBypassingLockScreen() {
         coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
+        val onBeforeRenderListListener =
+            withArgCaptor<OnBeforeRenderListListener> {
+                verify(pipeline).addOnBeforeRenderListListener(capture())
+            }
 
         whenever(lockscreenUserManager.currentUserId).thenReturn(1)
         whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -227,9 +575,11 @@
         whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
         whenever(statusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD)
         whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(any()))
-                .thenReturn(true)
-
+            .thenReturn(true)
         val entry = fakeNotification(2, true)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+        whenever(sensitiveNotificationProtectionController.shouldProtectNotification(any()))
+            .thenReturn(true)
 
         onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
 
@@ -237,15 +587,11 @@
     }
 
     private fun fakeNotification(notifUserId: Int, needsRedaction: Boolean): ListEntry {
-        val mockUserHandle = mock<UserHandle>().apply {
-            whenever(identifier).thenReturn(notifUserId)
-        }
-        val mockSbn: StatusBarNotification = mock<StatusBarNotification>().apply {
-            whenever(user).thenReturn(mockUserHandle)
-        }
-        val mockEntry = mock<NotificationEntry>().apply {
-            whenever(sbn).thenReturn(mockSbn)
-        }
+        val mockUserHandle =
+            mock<UserHandle>().apply { whenever(identifier).thenReturn(notifUserId) }
+        val mockSbn: StatusBarNotification =
+            mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) }
+        val mockEntry = mock<NotificationEntry>().apply { whenever(sbn).thenReturn(mockSbn) }
         whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction)
         whenever(mockEntry.rowExists()).thenReturn(true)
         return object : ListEntry("key", 0) {
@@ -268,6 +614,8 @@
             @BindsInstance statusBarStateController: StatusBarStateController,
             @BindsInstance keyguardStateController: KeyguardStateController,
             @BindsInstance selectedUserInteractor: SelectedUserInteractor,
+            @BindsInstance
+            sensitiveNotificationProtectionController: SensitiveNotificationProtectionController,
         ): TestSensitiveContentCoordinatorComponent
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 89f826b..1ab4c32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
+import static com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
 import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
@@ -32,6 +33,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
@@ -39,6 +41,7 @@
 
 import android.metrics.LogMaker;
 import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
@@ -101,6 +104,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.settings.SecureSettings;
@@ -172,10 +176,16 @@
     @Mock private ActivityStarter mActivityStarter;
     @Mock private KeyguardTransitionRepository mKeyguardTransitionRepo;
     @Mock private NotificationListViewBinder mViewBinder;
+    @Mock
+    private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController;
+
+    @Captor
+    private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor;
 
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
 
+
     private final ActiveNotificationListRepository mActiveNotificationsRepository =
             new ActiveNotificationListRepository();
 
@@ -386,6 +396,23 @@
     }
 
     @Test
+    public void testOnUserChange_verifyNotSensitive() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        initController(/* viewIsAttached= */ true);
+
+        ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
+                .forClass(UserChangedListener.class);
+
+        verify(mNotificationLockscreenUserManager)
+                .addUserChangedListener(userChangedCaptor.capture());
+        reset(mNotificationStackScrollLayout);
+
+        UserChangedListener changedListener = userChangedCaptor.getValue();
+        changedListener.onUserChanged(0);
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+    }
+
+    @Test
     public void testOnUserChange_verifySensitiveProfile() {
         when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
         initController(/* viewIsAttached= */ true);
@@ -403,6 +430,80 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnUserChange_verifyNotSensitive_screenshareNotificationHidingEnabled() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+
+        ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
+                .forClass(UserChangedListener.class);
+
+        verify(mNotificationLockscreenUserManager)
+                .addUserChangedListener(userChangedCaptor.capture());
+        reset(mNotificationStackScrollLayout);
+
+        UserChangedListener changedListener = userChangedCaptor.getValue();
+        changedListener.onUserChanged(0);
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnUserChange_verifySensitiveProfile_screenshareNotificationHidingEnabled() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+
+        ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
+                .forClass(UserChangedListener.class);
+
+        verify(mNotificationLockscreenUserManager)
+                .addUserChangedListener(userChangedCaptor.capture());
+        reset(mNotificationStackScrollLayout);
+
+        UserChangedListener changedListener = userChangedCaptor.getValue();
+        changedListener.onUserChanged(0);
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnUserChange_verifySensitiveActive_screenshareNotificationHidingEnabled() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+        initController(/* viewIsAttached= */ true);
+
+        ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
+                .forClass(UserChangedListener.class);
+
+        verify(mNotificationLockscreenUserManager)
+                .addUserChangedListener(userChangedCaptor.capture());
+        reset(mNotificationStackScrollLayout);
+
+        UserChangedListener changedListener = userChangedCaptor.getValue();
+        changedListener.onUserChanged(0);
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+    }
+
+    @Test
+    public void testOnStatePostChange_verifyNotSensitive() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+    }
+
+    @Test
     public void testOnStatePostChange_verifyIfProfileIsPublic() {
         when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
 
@@ -418,6 +519,194 @@
     }
 
     @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnStatePostChange_verifyNotSensitive_screenshareNotificationHidingEnabled() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnStatePostChange_verifyIfProfileIsPublic_screenshareNotificationHidingEnabled(
+    ) {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnStatePostChange_verifyIfSensitiveActive_screenshareNotificationHidingEnabled(
+    ) {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+    }
+
+    @Test
+    public void testOnStatePostChange_goingFullShade_verifyNotSensitive() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(true, false);
+    }
+
+    @Test
+    public void testOnStatePostChange_goingFullShade_verifyIfProfileIsPublic() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+        when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(true, true);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnStatePostChange_goingFullShade_verifyNotSensitive_screenshareHideEnabled(
+    ) {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(true, false);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnStatePostChange_goingFullShade_verifyProfileIsPublic_screenshareHideEnabled(
+    ) {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+        when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(true, true);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnStatePostChange_goingFullShade_verifySensitiveActive_screenshareHideEnabled(
+    ) {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+
+        stateListener.onStatePostChange();
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnProjectionStateChanged_verifyNotSensitive() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive())
+                .thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSensitiveNotificationProtectionController)
+                .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture());
+
+        mSensitiveStateListenerArgumentCaptor.getValue().run();
+
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnProjectionStateChanged_verifyIfProfileIsPublic() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSensitiveNotificationProtectionController)
+                .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture());
+
+        mSensitiveStateListenerArgumentCaptor.getValue().run();
+
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void testOnProjectionStateChanged_verifyIfSensitiveActive() {
+        when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+
+        initController(/* viewIsAttached= */ true);
+        verify(mSensitiveNotificationProtectionController)
+                .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture());
+
+        mSensitiveStateListenerArgumentCaptor.getValue().run();
+
+        verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+    }
+
+    @Test
     public void testOnMenuShownLogging() {
         ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS);
         when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker(
@@ -666,6 +955,20 @@
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean());
     }
 
+    @Test
+    @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void sensitiveNotificationProtectionControllerListenerNotRegistered() {
+        initController(/* viewIsAttached= */ true);
+        verifyZeroInteractions(mSensitiveNotificationProtectionController);
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    public void sensitiveNotificationProtectionControllerListenerRegistered() {
+        initController(/* viewIsAttached= */ true);
+        verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any());
+    }
+
     private LogMaker logMatcher(int category, int type) {
         return argThat(new LogMatcher(category, type));
     }
@@ -744,7 +1047,8 @@
                 mSecureSettings,
                 mock(NotificationDismissibilityProvider.class),
                 mActivityStarter,
-                new ResourcesSplitShadeStateController());
+                new ResourcesSplitShadeStateController(),
+                mSensitiveNotificationProtectionController);
     }
 
     static class LogMatcher implements ArgumentMatcher<LogMaker> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index a824bc4..06298b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -25,6 +25,9 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -33,20 +36,19 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.shade.mockLargeScreenHeaderHelper
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -68,6 +70,7 @@
     val keyguardInteractor = kosmos.keyguardInteractor
     val keyguardRootViewModel = kosmos.keyguardRootViewModel
     val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    val communalInteractor = kosmos.communalInteractor
     val shadeRepository = kosmos.shadeRepository
     val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor
     val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper
@@ -214,6 +217,61 @@
         }
 
     @Test
+    fun glanceableHubAlpha() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.glanceableHubAlpha)
+
+            // Start on lockscreen
+            showLockscreen()
+            assertThat(alpha).isEqualTo(1f)
+
+            // Start transitioning to glanceable hub
+            val progress = 0.6f
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    value = 0f,
+                )
+            )
+            runCurrent()
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.RUNNING,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    value = progress,
+                )
+            )
+            runCurrent()
+            // Expected alpha is inverse of progress as notifications are fading away
+            assertThat(alpha).isEqualTo(1 - progress)
+
+            // Finish transition to glanceable hub
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    value = 1f,
+                )
+            )
+            val idleTransitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+                )
+            communalInteractor.setTransitionState(idleTransitionState)
+            runCurrent()
+            assertThat(alpha).isEqualTo(0f)
+
+            // While state is GLANCEABLE_HUB, verify alpha is restored to full if glanceable hub is
+            // not fully visible.
+            shadeRepository.setLockscreenShadeExpansion(0.1f)
+            assertThat(alpha).isEqualTo(1f)
+        }
+
+    @Test
     fun validateMarginTop() =
         testScope.runTest {
             overrideResource(R.bool.config_use_large_screen_shade_header, false)
@@ -302,6 +360,43 @@
         }
 
     @Test
+    fun isOnGlanceableHubWithoutShade() =
+        testScope.runTest {
+            val isOnGlanceableHubWithoutShade by
+                collectLastValue(underTest.isOnGlanceableHubWithoutShade)
+
+            // Start on lockscreen
+            showLockscreen()
+            assertThat(isOnGlanceableHubWithoutShade).isFalse()
+
+            // Move to glanceable hub
+            val idleTransitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+                )
+            communalInteractor.setTransitionState(idleTransitionState)
+            runCurrent()
+            assertThat(isOnGlanceableHubWithoutShade).isTrue()
+
+            // While state is GLANCEABLE_HUB, validate variations of both shade and qs expansion
+            shadeRepository.setLockscreenShadeExpansion(0.1f)
+            shadeRepository.setQsExpansion(0f)
+            assertThat(isOnGlanceableHubWithoutShade).isFalse()
+
+            shadeRepository.setLockscreenShadeExpansion(0.1f)
+            shadeRepository.setQsExpansion(0.1f)
+            assertThat(isOnGlanceableHubWithoutShade).isFalse()
+
+            shadeRepository.setLockscreenShadeExpansion(0f)
+            shadeRepository.setQsExpansion(0.1f)
+            assertThat(isOnGlanceableHubWithoutShade).isFalse()
+
+            shadeRepository.setLockscreenShadeExpansion(0f)
+            shadeRepository.setQsExpansion(0f)
+            assertThat(isOnGlanceableHubWithoutShade).isTrue()
+        }
+
+    @Test
     fun boundsOnLockscreenNotInSplitShade() =
         testScope.runTest {
             val bounds by collectLastValue(underTest.bounds)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 4827c92..b3a47d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -48,7 +48,6 @@
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
 import android.graphics.Color;
-import android.os.Handler;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.util.MathUtils;
@@ -66,6 +65,7 @@
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -134,7 +134,7 @@
     @Mock private AlarmManager mAlarmManager;
     @Mock private DozeParameters mDozeParameters;
     @Mock private LightBarController mLightBarController;
-    @Mock private DelayedWakeLock.Builder mDelayedWakeLockBuilder;
+    @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory;
     @Mock private DelayedWakeLock mWakeLock;
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@@ -145,6 +145,7 @@
     @Mock private AlternateBouncerToGoneTransitionViewModel
             mAlternateBouncerToGoneTransitionViewModel;
     @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    @Mock private KeyguardInteractor mKeyguardInteractor;
     private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository();
     @Mock private CoroutineDispatcher mMainDispatcher;
     @Mock private TypedArray mMockTypedArray;
@@ -260,11 +261,7 @@
         }).when(mLightBarController).setScrimState(
                 any(ScrimState.class), anyFloat(), any(GradientColors.class));
 
-        when(mDelayedWakeLockBuilder.setHandler(any(Handler.class)))
-                .thenReturn(mDelayedWakeLockBuilder);
-        when(mDelayedWakeLockBuilder.setTag(any(String.class)))
-                .thenReturn(mDelayedWakeLockBuilder);
-        when(mDelayedWakeLockBuilder.build()).thenReturn(mWakeLock);
+        when(mDelayedWakeLockFactory.create(any(String.class))).thenReturn(mWakeLock);
         when(mDockManager.isDocked()).thenReturn(false);
 
         when(mKeyguardTransitionInteractor.transition(any(), any()))
@@ -279,7 +276,7 @@
                 mDozeParameters,
                 mAlarmManager,
                 mKeyguardStateController,
-                mDelayedWakeLockBuilder,
+                mDelayedWakeLockFactory,
                 new FakeHandler(mLooper.getLooper()),
                 mKeyguardUpdateMonitor,
                 mDockManager,
@@ -292,6 +289,7 @@
                 mPrimaryBouncerToGoneTransitionViewModel,
                 mAlternateBouncerToGoneTransitionViewModel,
                 mKeyguardTransitionInteractor,
+                mKeyguardInteractor,
                 mWallpaperRepository,
                 mMainDispatcher,
                 mLinearLargeScreenShadeInterpolator);
@@ -987,7 +985,7 @@
                 mDozeParameters,
                 mAlarmManager,
                 mKeyguardStateController,
-                mDelayedWakeLockBuilder,
+                mDelayedWakeLockFactory,
                 new FakeHandler(mLooper.getLooper()),
                 mKeyguardUpdateMonitor,
                 mDockManager,
@@ -1000,6 +998,7 @@
                 mPrimaryBouncerToGoneTransitionViewModel,
                 mAlternateBouncerToGoneTransitionViewModel,
                 mKeyguardTransitionInteractor,
+                mKeyguardInteractor,
                 mWallpaperRepository,
                 mMainDispatcher,
                 mLinearLargeScreenShadeInterpolator);
diff --git a/packages/SystemUI/tests/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
index 1f8cc54..6785de93 100644
--- a/packages/SystemUI/tests/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
@@ -168,6 +168,7 @@
                 assertThat(conn.carrierName.value)
                     .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}"))
                 assertThat(conn.hasPrioritizedNetworkCapabilities.value).isEqualTo(model.slice)
+                assertThat(conn.isNonTerrestrial.value).isEqualTo(model.ntn)
 
                 // TODO(b/261029387): check these once we start handling them
                 assertThat(conn.isEmergencyOnly.value).isFalse()
@@ -194,6 +195,7 @@
         val roaming: Boolean,
         val name: String,
         val slice: Boolean,
+        val ntn: Boolean,
     ) {
         override fun toString(): String {
             return "INPUT(level=$level, " +
@@ -205,7 +207,8 @@
                 "carrierNetworkChange=$carrierNetworkChange, " +
                 "roaming=$roaming, " +
                 "name=$name," +
-                "slice=$slice)"
+                "slice=$slice" +
+                "ntn=$ntn)"
         }
 
         // Convenience for iterating test data and creating new cases
@@ -220,6 +223,7 @@
             roaming: Boolean? = null,
             name: String? = null,
             slice: Boolean? = null,
+            ntn: Boolean? = null,
         ): TestCase =
             TestCase(
                 level = level ?: this.level,
@@ -232,6 +236,7 @@
                 roaming = roaming ?: this.roaming,
                 name = name ?: this.name,
                 slice = slice ?: this.slice,
+                ntn = ntn ?: this.ntn,
             )
     }
 
@@ -262,6 +267,7 @@
         private val roaming = listOf(false, true)
         private val names = listOf("name 1", "name 2")
         private val slice = listOf(false, true)
+        private val ntn = listOf(false, true)
 
         @Parameters(name = "{0}") @JvmStatic fun data() = testData()
 
@@ -300,6 +306,7 @@
                     roaming.first(),
                     names.first(),
                     slice.first(),
+                    ntn.first(),
                 )
 
             val tail =
@@ -312,7 +319,8 @@
                         carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
                         roaming.map { baseCase.modifiedBy(roaming = it) },
                         names.map { baseCase.modifiedBy(name = it) },
-                        slice.map { baseCase.modifiedBy(slice = it) }
+                        slice.map { baseCase.modifiedBy(slice = it) },
+                        ntn.map { baseCase.modifiedBy(ntn = it) }
                     )
                     .flatten()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index 03814bd..b958f35 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -579,6 +579,7 @@
                 assertThat(conn.carrierName.value)
                     .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}"))
                 assertThat(conn.hasPrioritizedNetworkCapabilities.value).isEqualTo(model.slice)
+                assertThat(conn.isNonTerrestrial.value).isEqualTo(model.ntn)
 
                 // TODO(b/261029387) check these once we start handling them
                 assertThat(conn.isEmergencyOnly.value).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 9d6f315..9855651 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -21,6 +21,7 @@
 import android.content.Intent
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
+import android.platform.test.annotations.EnableFlags
 import android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WLAN
 import android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN
 import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
@@ -963,6 +964,31 @@
         }
 
     @Test
+    @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    fun isNonTerrestrial_updatesFromServiceState() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isNonTerrestrial)
+
+            // Lambda makes it a little clearer what we are testing IMO
+            val serviceStateCreator = { ntn: Boolean ->
+                mock<ServiceState>().also {
+                    whenever(it.isUsingNonTerrestrialNetwork).thenReturn(ntn)
+                }
+            }
+
+            // Starts out false
+            assertThat(latest).isFalse()
+
+            getTelephonyCallbackForType<ServiceStateListener>()
+                .onServiceStateChanged(serviceStateCreator(true))
+            assertThat(latest).isTrue()
+
+            getTelephonyCallbackForType<ServiceStateListener>()
+                .onServiceStateChanged(serviceStateCreator(false))
+            assertThat(latest).isFalse()
+        }
+
+    @Test
     fun numberOfLevels_usesCarrierConfig() =
         testScope.runTest {
             var latest: Int? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 02e6fd5..0e0d489 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -35,6 +35,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
@@ -87,6 +88,7 @@
                     Optional.empty(),
                     dispatcher,
                     testScope.backgroundScope,
+                    FakeLogBuffer.Factory.create(),
                     systemClock,
                 )
 
@@ -100,6 +102,45 @@
         }
 
     @Test
+    fun satelliteManagerThrows_checkSupportDoesNotCrash() =
+        testScope.runTest {
+            whenever(satelliteManager.requestIsSatelliteSupported(any(), any()))
+                .thenThrow(IllegalStateException())
+
+            systemClock.setUptimeMillis(Process.getStartUptimeMillis() + MIN_UPTIME)
+
+            underTest =
+                DeviceBasedSatelliteRepositoryImpl(
+                    Optional.of(satelliteManager),
+                    dispatcher,
+                    testScope.backgroundScope,
+                    FakeLogBuffer.Factory.create(),
+                    systemClock,
+                )
+
+            runCurrent()
+
+            // Creating the repo does not crash, and we consider the feature not to be supported
+            assertThat(underTest.satelliteSupport.value).isEqualTo(SatelliteSupport.NotSupported)
+        }
+
+    @Test
+    fun satelliteManagerThrows_doesNotCrash() =
+        testScope.runTest {
+            setupDefaultRepo()
+
+            whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any()))
+                .thenThrow(SatelliteException(13))
+
+            val conn by collectLastValue(underTest.connectionState)
+            val strength by collectLastValue(underTest.signalStrength)
+
+            // Flows have not emitted, we haven't crashed
+            assertThat(conn).isNull()
+            assertThat(strength).isNull()
+        }
+
+    @Test
     fun connectionState_mapsFromSatelliteModemState() =
         testScope.runTest {
             setupDefaultRepo()
@@ -380,6 +421,7 @@
                 if (satMan != null) Optional.of(satMan) else Optional.empty(),
                 dispatcher,
                 testScope.backgroundScope,
+                FakeLogBuffer.Factory.create(),
                 systemClock,
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
new file mode 100644
index 0000000..21c038a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class DeviceBasedSatelliteViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: DeviceBasedSatelliteViewModel
+    private lateinit var interactor: DeviceBasedSatelliteInteractor
+
+    private val repo = FakeDeviceBasedSatelliteRepository()
+    private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+    private val testScope = TestScope()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        interactor =
+            DeviceBasedSatelliteInteractor(
+                repo,
+                mobileIconsInteractor,
+                testScope.backgroundScope,
+            )
+
+        underTest =
+            DeviceBasedSatelliteViewModel(
+                interactor,
+                testScope.backgroundScope,
+            )
+    }
+
+    @Test
+    fun icon_nullWhenShouldNotShow_satelliteNotAllowed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is not allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = false
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+
+            // THEN icon is null because we should not be showing it
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun icon_nullWhenShouldNotShow_notAllOos() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are not OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = true
+
+            // THEN icon is null because we have service
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun icon_satelliteIsOff() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+
+            // THEN icon is null because we have service
+            assertThat(latest).isInstanceOf(Icon::class.java)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt
new file mode 100644
index 0000000..ca9df57
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.view
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.StatusBarIconView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Being a simple subclass of [ModernStatusBarView], use the same basic test cases to verify the
+ * root behavior, and add testing for the new [SingleBindableStatusBarIconView.withDefaultBinding]
+ * method.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class SingleBindableStatusBarIconViewTest : SysuiTestCase() {
+    private lateinit var binding: SingleBindableStatusBarIconViewBinding
+
+    // Visibility is outsourced to view-models. This simulates it
+    private var isVisible = true
+    private var visibilityFn: () -> Boolean = { isVisible }
+
+    @Test
+    fun initView_hasCorrectSlot() {
+        val view = createAndInitView()
+
+        assertThat(view.slot).isEqualTo(SLOT_NAME)
+    }
+
+    @Test
+    fun getVisibleState_icon_returnsIcon() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_ICON)
+    }
+
+    @Test
+    fun getVisibleState_dot_returnsDot() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_DOT)
+    }
+
+    @Test
+    fun getVisibleState_hidden_returnsHidden() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_HIDDEN)
+    }
+
+    @Test
+    fun onDarkChanged_bindingReceivesIconAndDecorTint() {
+        val view = createAndInitView()
+
+        view.onDarkChangedWithContrast(arrayListOf(), 0x12345678, 0x12344321)
+
+        assertThat(binding.iconTint).isEqualTo(0x12345678)
+        assertThat(binding.decorTint).isEqualTo(0x12345678)
+    }
+
+    @Test
+    fun setStaticDrawableColor_bindingReceivesIconTint() {
+        val view = createAndInitView()
+
+        view.setStaticDrawableColor(0x12345678, 0x12344321)
+
+        assertThat(binding.iconTint).isEqualTo(0x12345678)
+    }
+
+    @Test
+    fun setDecorColor_bindingReceivesDecorColor() {
+        val view = createAndInitView()
+
+        view.setDecorColor(0x23456789)
+
+        assertThat(binding.decorTint).isEqualTo(0x23456789)
+    }
+
+    @Test
+    fun isIconVisible_usesBinding_true() {
+        val view = createAndInitView()
+
+        isVisible = true
+
+        assertThat(view.isIconVisible).isEqualTo(true)
+    }
+
+    @Test
+    fun isIconVisible_usesBinding_false() {
+        val view = createAndInitView()
+
+        isVisible = false
+
+        assertThat(view.isIconVisible).isEqualTo(false)
+    }
+
+    @Test
+    fun getDrawingRect_takesTranslationIntoAccount() {
+        val view = createAndInitView()
+
+        view.translationX = 50f
+        view.translationY = 60f
+
+        val drawingRect = Rect()
+        view.getDrawingRect(drawingRect)
+
+        assertThat(drawingRect.left).isEqualTo(view.left + 50)
+        assertThat(drawingRect.right).isEqualTo(view.right + 50)
+        assertThat(drawingRect.top).isEqualTo(view.top + 60)
+        assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60)
+    }
+
+    private fun createAndInitView(): SingleBindableStatusBarIconView {
+        val view = SingleBindableStatusBarIconView.createView(context)
+        binding = SingleBindableStatusBarIconView.withDefaultBinding(view, visibilityFn) {}
+        view.initView(SLOT_NAME) { binding }
+        return view
+    }
+
+    companion object {
+        private const val SLOT_NAME = "test_slot"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
new file mode 100644
index 0000000..cd5d5ed
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.app.Notification
+import android.media.projection.MediaProjectionInfo
+import android.media.projection.MediaProjectionManager
+import android.os.Handler
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SensitiveNotificationProtectionControllerTest : SysuiTestCase() {
+    @Mock private lateinit var handler: Handler
+
+    @Mock private lateinit var mediaProjectionManager: MediaProjectionManager
+
+    @Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo
+
+    @Mock private lateinit var listener1: Runnable
+    @Mock private lateinit var listener2: Runnable
+    @Mock private lateinit var listener3: Runnable
+
+    @Captor
+    private lateinit var mediaProjectionCallbackCaptor:
+        ArgumentCaptor<MediaProjectionManager.Callback>
+
+    private lateinit var controller: SensitiveNotificationProtectionControllerImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+
+        controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler)
+
+        // Obtain useful MediaProjectionCallback
+        verify(mediaProjectionManager).addCallback(mediaProjectionCallbackCaptor.capture(), any())
+    }
+
+    @Test
+    fun init_flagEnabled_registerMediaProjectionManagerCallback() {
+        assertNotNull(mediaProjectionCallbackCaptor.value)
+    }
+
+    @Test
+    fun init_flagDisabled_noRegisterMediaProjectionManagerCallback() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+        reset(mediaProjectionManager)
+
+        controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler)
+
+        verifyZeroInteractions(mediaProjectionManager)
+    }
+
+    @Test
+    fun registerSensitiveStateListener_singleListener() {
+        controller.registerSensitiveStateListener(listener1)
+
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+        verify(listener1, times(2)).run()
+    }
+
+    @Test
+    fun registerSensitiveStateListener_multipleListeners() {
+        controller.registerSensitiveStateListener(listener1)
+        controller.registerSensitiveStateListener(listener2)
+
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+        verify(listener1, times(2)).run()
+        verify(listener2, times(2)).run()
+    }
+
+    @Test
+    fun registerSensitiveStateListener_afterProjectionActive() {
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+        controller.registerSensitiveStateListener(listener1)
+        verifyZeroInteractions(listener1)
+
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+        verify(listener1).run()
+    }
+
+    @Test
+    fun unregisterSensitiveStateListener_singleListener() {
+        controller.registerSensitiveStateListener(listener1)
+
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+        verify(listener1, times(2)).run()
+
+        controller.unregisterSensitiveStateListener(listener1)
+
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+        verifyNoMoreInteractions(listener1)
+    }
+
+    @Test
+    fun unregisterSensitiveStateListener_multipleListeners() {
+        controller.registerSensitiveStateListener(listener1)
+        controller.registerSensitiveStateListener(listener2)
+        controller.registerSensitiveStateListener(listener3)
+
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+        verify(listener1, times(2)).run()
+        verify(listener2, times(2)).run()
+        verify(listener3, times(2)).run()
+
+        controller.unregisterSensitiveStateListener(listener1)
+        controller.unregisterSensitiveStateListener(listener2)
+
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+        verifyNoMoreInteractions(listener1)
+        verifyNoMoreInteractions(listener2)
+        verify(listener3, times(4)).run()
+    }
+
+    @Test
+    fun isSensitiveStateActive_projectionInactive_false() {
+        assertFalse(controller.isSensitiveStateActive)
+    }
+
+    @Test
+    fun isSensitiveStateActive_projectionActive_true() {
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+        assertTrue(controller.isSensitiveStateActive)
+    }
+
+    @Test
+    fun isSensitiveStateActive_projectionInactiveAfterActive_false() {
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+        assertFalse(controller.isSensitiveStateActive)
+    }
+
+    @Test
+    fun isSensitiveStateActive_projectionActiveAfterInactive_true() {
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+        assertTrue(controller.isSensitiveStateActive)
+    }
+
+    @Test
+    fun shouldProtectNotification_projectionInactive_false() {
+        val notificationEntry = mock(NotificationEntry::class.java)
+
+        assertFalse(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
+    fun shouldProtectNotification_projectionActive_fgsNotification_false() {
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+        val notificationEntry = mock(NotificationEntry::class.java)
+        val sbn = mock(StatusBarNotification::class.java)
+        val notification = mock(Notification::class.java)
+        `when`(notificationEntry.sbn).thenReturn(sbn)
+        `when`(sbn.notification).thenReturn(notification)
+        `when`(notification.isFgsOrUij).thenReturn(true)
+
+        assertFalse(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
+    fun shouldProtectNotification_projectionActive_notFgsNotification_true() {
+        mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+        val notificationEntry = mock(NotificationEntry::class.java)
+        val sbn = mock(StatusBarNotification::class.java)
+        val notification = mock(Notification::class.java)
+        `when`(notificationEntry.sbn).thenReturn(sbn)
+        `when`(sbn.notification).thenReturn(notification)
+        `when`(notification.isFgsOrUij).thenReturn(false)
+
+        assertTrue(controller.shouldProtectNotification(notificationEntry))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 8c823b2..d839da1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -32,6 +32,7 @@
 
 import static org.junit.Assume.assumeNotNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -63,6 +64,7 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.testing.UiEventLoggerFake;
+import com.android.keyguard.TestScopeProvider;
 import com.android.systemui.Prefs;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
@@ -72,6 +74,7 @@
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.plugins.VolumeDialogController.State;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DevicePostureController;
@@ -79,6 +82,7 @@
 import com.android.systemui.statusbar.policy.FakeConfigurationController;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import dagger.Lazy;
 
@@ -97,6 +101,8 @@
 import java.util.Arrays;
 import java.util.function.Predicate;
 
+import kotlinx.coroutines.Dispatchers;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -138,7 +144,6 @@
     DevicePostureController mPostureController;
     @Mock
     private Lazy<SecureSettings> mLazySecureSettings;
-
     private final CsdWarningDialog.Factory mCsdWarningDialogFactory =
             new CsdWarningDialog.Factory() {
         @Override
@@ -146,6 +151,8 @@
             return mCsdWarningDialog;
         }
     };
+    @Mock
+    private VibratorHelper mVibratorHelper;
 
     private int mLongestHideShowAnimationDuration = 250;
     private FakeSettings mSecureSettings;
@@ -180,6 +187,8 @@
 
         when(mLazySecureSettings.get()).thenReturn(mSecureSettings);
 
+        when(mVibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(new int[]{0});
+
         mDialog = new VolumeDialogImpl(
                 getContext(),
                 mVolumeDialogController,
@@ -195,7 +204,11 @@
                 mPostureController,
                 mTestableLooper.getLooper(),
                 mDumpManager,
-                mLazySecureSettings);
+                mLazySecureSettings,
+                mVibratorHelper,
+                Dispatchers.getUnconfined(),
+                TestScopeProvider.getTestScope(),
+                new FakeSystemClock());
         mDialog.init(0, null);
         State state = createShellState();
         mDialog.onStateChangedH(state);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 589f7c2..93d8dcc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -100,7 +100,6 @@
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
 import com.android.systemui.communal.domain.interactor.CommunalInteractor;
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -443,8 +442,7 @@
                         () -> keyguardInteractor,
                         () -> mFromLockscreenTransitionInteractor,
                         () -> mFromPrimaryBouncerTransitionInteractor);
-        CommunalInteractor communalInteractor =
-                CommunalInteractorFactory.create().getCommunalInteractor();
+        CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor();
 
         mFromLockscreenTransitionInteractor = new FromLockscreenTransitionInteractor(
                 keyguardTransitionRepository,
@@ -536,7 +534,8 @@
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
-                mSceneContainerFlags
+                mSceneContainerFlags,
+                mKosmos::getCommunalInteractor
         );
         mNotificationShadeWindowController.fetchWindowRootView();
         mNotificationShadeWindowController.attach();
diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
index b820ca6..1afe56f 100644
--- a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
+++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
@@ -22,6 +22,7 @@
 import android.os.Looper;
 import android.os.SystemClock;
 import android.util.AndroidRuntimeException;
+import android.util.Singleton;
 import android.view.Choreographer;
 
 import com.android.internal.util.Preconditions;
@@ -67,7 +68,12 @@
 public final class AnimatorTestRule implements TestRule {
 
     private final Object mLock = new Object();
-    private final TestHandler mTestHandler = new TestHandler();
+    private final Singleton<TestHandler> mTestHandler = new Singleton<>() {
+        @Override
+        protected TestHandler create() {
+            return new TestHandler();
+        }
+    };
     private final long mStartTime;
     private long mTotalTimeDelta = 0;
 
@@ -95,16 +101,17 @@
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
-                AnimationHandler objAtStart = AnimationHandler.setTestHandler(mTestHandler);
+                final TestHandler testHandler = mTestHandler.get();
+                AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler);
                 try {
                     base.evaluate();
                 } finally {
                     AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart);
-                    if (mTestHandler != objAtEnd) {
+                    if (testHandler != objAtEnd) {
                         // pass or fail, inner logic not restoring the handler needs to be reported.
                         // noinspection ThrowFromFinallyBlock
                         throw new IllegalStateException("Test handler was altered: expected="
-                                + mTestHandler + " actual=" + objAtEnd);
+                                + testHandler + " actual=" + objAtEnd);
                     }
                 }
             }
@@ -125,8 +132,9 @@
     public void initNewAnimators() {
         requireLooper("AnimationTestRule#initNewAnimators()");
         long currentTime = getCurrentTime();
-        List<AnimationFrameCallback> newCallbacks = new ArrayList<>(mTestHandler.mNewCallbacks);
-        mTestHandler.mNewCallbacks.clear();
+        final TestHandler testHandler = mTestHandler.get();
+        List<AnimationFrameCallback> newCallbacks = new ArrayList<>(testHandler.mNewCallbacks);
+        testHandler.mNewCallbacks.clear();
         for (AnimationFrameCallback newCallback : newCallbacks) {
             newCallback.doAnimationFrame(currentTime);
         }
@@ -158,9 +166,10 @@
     public void advanceTimeBy(long timeDelta, @Nullable Consumer<Long> preFrameAction) {
         Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative");
         requireLooper("AnimationTestRule#advanceTimeBy(long)");
+        final TestHandler testHandler = mTestHandler.get();
         if (timeDelta == 0) {
             // If time is not being advanced, all animators will get a tick; don't double tick these
-            mTestHandler.mNewCallbacks.clear();
+            testHandler.mNewCallbacks.clear();
         } else {
             // before advancing time, start new animators with the current time
             initNewAnimators();
@@ -172,10 +181,10 @@
         if (preFrameAction != null) {
             preFrameAction.accept(timeDelta);
             // After letting other code run, clear any new callbacks to avoid double-ticking them
-            mTestHandler.mNewCallbacks.clear();
+            testHandler.mNewCallbacks.clear();
         }
         // produce a frame
-        mTestHandler.doFrame();
+        testHandler.doFrame();
     }
 
     /**
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt
similarity index 76%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt
index 33143fe..e5121d5 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package android.app
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.keyguardManager by Kosmos.Fixture { mock<KeyguardManager>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt
similarity index 69%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt
index 33143fe..4e2683b 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package android.os
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.concurrency.mockExecutorHandler
+
+val Kosmos.fakeExecutorHandler by Kosmos.Fixture { mockExecutorHandler(fakeExecutor) }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
similarity index 72%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
index 33143fe..fb51f0f 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package android.service.dream
 
-parcelable WlcLDeviceInfo;
+import android.service.dreams.IDreamManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.dreamManager by Kosmos.Fixture { mock<IDreamManager>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt
similarity index 74%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt
index 33143fe..d9ea5e9 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.internal.widget
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.lockPatternUtils by Kosmos.Fixture { mock<LockPatternUtils>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
similarity index 74%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
index 33143fe..7185b7c 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui
 
-parcelable WlcLDeviceInfo;
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.activityIntentHelper by Kosmos.Fixture { ActivityIntentHelper(applicationContext) }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
similarity index 78%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
index 33143fe..128f58b 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.animation
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
similarity index 75%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
index 33143fe..b7d6f3a 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.assist
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.assistManager by Kosmos.Fixture { mock<AssistManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
deleted file mode 100644
index 1ba8630..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.communal.domain.interactor
-
-import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
-import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
-import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
-import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
-import com.android.systemui.communal.widgets.CommunalAppWidgetHost
-import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
-import com.android.systemui.util.mockito.mock
-import kotlinx.coroutines.test.TestScope
-
-// TODO(b/319335645): get rid of me and use kosmos.
-object CommunalInteractorFactory {
-
-    @JvmOverloads
-    @JvmStatic
-    fun create(
-        testScope: TestScope = TestScope(),
-        communalRepository: FakeCommunalRepository =
-            FakeCommunalRepository(testScope.backgroundScope),
-        keyguardRepository: FakeKeyguardRepository = FakeKeyguardRepository(),
-        widgetRepository: FakeCommunalWidgetRepository =
-            FakeCommunalWidgetRepository(testScope.backgroundScope),
-        mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(),
-        smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(),
-        tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(),
-        communalPrefsRepository: FakeCommunalPrefsRepository = FakeCommunalPrefsRepository(),
-        appWidgetHost: CommunalAppWidgetHost = mock(),
-        editWidgetsActivityStarter: EditWidgetsActivityStarter = mock(),
-    ): WithDependencies {
-        val keyguardInteractor =
-            KeyguardInteractorFactory.create(repository = keyguardRepository).keyguardInteractor
-        val communalTutorialInteractor =
-            CommunalTutorialInteractor(
-                testScope.backgroundScope,
-                tutorialRepository,
-                keyguardInteractor,
-                communalRepository,
-            )
-
-        return WithDependencies(
-            testScope,
-            communalRepository,
-            widgetRepository,
-            communalPrefsRepository,
-            mediaRepository,
-            smartspaceRepository,
-            tutorialRepository,
-            keyguardRepository,
-            keyguardInteractor,
-            communalTutorialInteractor,
-            appWidgetHost,
-            editWidgetsActivityStarter,
-            CommunalInteractor(
-                testScope.backgroundScope,
-                communalRepository,
-                widgetRepository,
-                communalPrefsRepository,
-                mediaRepository,
-                smartspaceRepository,
-                keyguardInteractor,
-                appWidgetHost,
-                editWidgetsActivityStarter,
-            ),
-        )
-    }
-
-    data class WithDependencies(
-        val testScope: TestScope,
-        val communalRepository: FakeCommunalRepository,
-        val widgetRepository: FakeCommunalWidgetRepository,
-        val communalPrefsRepository: FakeCommunalPrefsRepository,
-        val mediaRepository: FakeCommunalMediaRepository,
-        val smartspaceRepository: FakeSmartspaceRepository,
-        val tutorialRepository: FakeCommunalTutorialRepository,
-        val keyguardRepository: FakeKeyguardRepository,
-        val keyguardInteractor: KeyguardInteractor,
-        val tutorialInteractor: CommunalTutorialInteractor,
-        val appWidgetHost: CommunalAppWidgetHost,
-        val editWidgetsActivityStarter: EditWidgetsActivityStarter,
-        val communalInteractor: CommunalInteractor,
-    )
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index 65579a6..1abf71f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -20,11 +20,13 @@
 import com.android.systemui.communal.data.repository.communalPrefsRepository
 import com.android.systemui.communal.data.repository.communalRepository
 import com.android.systemui.communal.data.repository.communalWidgetRepository
+import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.smartspace.data.repository.smartspaceRepository
+import com.android.systemui.user.data.repository.userRepository
 import com.android.systemui.util.mockito.mock
 
 val Kosmos.communalInteractor by Fixture {
@@ -35,8 +37,11 @@
         mediaRepository = communalMediaRepository,
         communalPrefsRepository = communalPrefsRepository,
         smartspaceRepository = smartspaceRepository,
+        userRepository = userRepository,
         appWidgetHost = mock(),
         keyguardInteractor = keyguardInteractor,
-        editWidgetsActivityStarter = mock(),
+        editWidgetsActivityStarter = editWidgetsActivityStarter,
     )
 }
+
+val Kosmos.editWidgetsActivityStarter by Fixture<EditWidgetsActivityStarter> { mock() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
index baba88d..adaea7c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
@@ -29,5 +29,6 @@
             communalTutorialRepository = communalTutorialRepository,
             keyguardInteractor = keyguardInteractor,
             communalRepository = communalRepository,
+            communalInteractor = communalInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt
new file mode 100644
index 0000000..68e1457
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.data.repository
+
+import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository
+import com.android.systemui.keyboard.stickykeys.shared.model.Locked
+import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeStickyKeysRepository : StickyKeysRepository {
+    override val settingEnabled: Flow<Boolean> = MutableStateFlow(true)
+    private val _stickyKeys: MutableStateFlow<LinkedHashMap<ModifierKey, Locked>> =
+        MutableStateFlow(LinkedHashMap())
+
+    override val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> = _stickyKeys
+
+    fun setStickyKeys(keys: LinkedHashMap<ModifierKey, Locked>) {
+        _stickyKeys.value = keys
+    }
+}
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt
similarity index 70%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt
index 33143fe..46f7355 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.keyboard.data.repository
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.keyboardRepository by Kosmos.Fixture { FakeKeyboardRepository() }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 59f56dd..5766f7a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -130,6 +130,8 @@
     private val _isEncryptedOrLockdown = MutableStateFlow(true)
     override val isEncryptedOrLockdown: Flow<Boolean> = _isEncryptedOrLockdown
 
+    override val topClippingBounds = MutableStateFlow<Int?>(null)
+
     override fun setQuickSettingsVisible(isVisible: Boolean) {
         _isQuickSettingsVisible.value = isVisible
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt
deleted file mode 100644
index 92193fd..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.domain.interactor
-
-import com.android.systemui.communal.data.repository.communalMediaRepository
-import com.android.systemui.communal.data.repository.communalPrefsRepository
-import com.android.systemui.communal.data.repository.communalRepository
-import com.android.systemui.communal.data.repository.communalWidgetRepository
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.widgets.CommunalAppWidgetHost
-import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.smartspace.data.repository.smartspaceRepository
-import org.mockito.Mockito.mock
-
-val Kosmos.communalInteractor by
-    Kosmos.Fixture {
-        CommunalInteractor(
-            applicationScope = applicationCoroutineScope,
-            communalRepository = communalRepository,
-            widgetRepository = communalWidgetRepository,
-            mediaRepository = communalMediaRepository,
-            communalPrefsRepository = communalPrefsRepository,
-            smartspaceRepository = smartspaceRepository,
-            keyguardInteractor = keyguardInteractor,
-            appWidgetHost = mock(CommunalAppWidgetHost::class.java),
-            editWidgetsActivityStarter = editWidgetsActivityStarter,
-        )
-    }
-
-val Kosmos.editWidgetsActivityStarter by
-    Kosmos.Fixture<EditWidgetsActivityStarter> { mock(EditWidgetsActivityStarter::class.java) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
index 294b5ba..ec17c48 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 5564767..d376f12 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
@@ -33,6 +34,7 @@
         deviceEntryInteractor = deviceEntryInteractor,
         dozeParameters = dozeParameters,
         keyguardInteractor = keyguardInteractor,
+        communalInteractor = communalInteractor,
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         notificationsKeyguardInteractor = notificationsKeyguardInteractor,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index cc0449d..321f944 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -72,6 +73,7 @@
     val powerInteractor by lazy { kosmos.powerInteractor }
     val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
     val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
+    val communalInteractor by lazy { kosmos.communalInteractor }
 
     init {
         kosmos.applicationContext = testCase.context
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
similarity index 75%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
index 33143fe..7cc5d6b 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.shade
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.shadeController by Kosmos.Fixture { mock<ShadeController>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
similarity index 74%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
index 33143fe..1ceab68 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.shade
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.shadeViewController by Kosmos.Fixture { mock<ShadeViewController>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt
similarity index 76%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt
index 33143fe..4dcd220 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.shade.data.repository
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.shadeAnimationRepository by Kosmos.Fixture { ShadeAnimationRepository() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt
similarity index 65%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt
index 33143fe..57b272f 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.shade.domain.interactor
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.data.repository.shadeAnimationRepository
+
+var Kosmos.shadeAnimationInteractor: ShadeAnimationInteractor by
+    Kosmos.Fixture { ShadeAnimationInteractorEmptyImpl(shadeAnimationRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
new file mode 100644
index 0000000..a75d2bc
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.notifications.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+
+val Kosmos.notificationSettingsRepository by
+    Kosmos.Fixture {
+        NotificationSettingsRepository(
+            scope = testScope.backgroundScope,
+            backgroundDispatcher = testDispatcher,
+            secureSettingsRepository = secureSettingsRepository,
+        )
+    }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt
similarity index 64%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt
index 33143fe..17b4603 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.shared.notifications.domain.interactor
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository
+
+val Kosmos.notificationSettingsInteractor by
+    Kosmos.Fixture { NotificationSettingsInteractor(notificationSettingsRepository) }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt
similarity index 65%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt
index 33143fe..552b09e 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.shared.settings.data.repository
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.secureSettingsRepository: SecureSettingsRepository by
+    Kosmos.Fixture { fakeSecureSettingsRepository }
+val Kosmos.fakeSecureSettingsRepository by Kosmos.Fixture { FakeSecureSettingsRepository() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt
similarity index 73%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt
index 33143fe..7b912ae 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationClickNotifier by Kosmos.Fixture { mock<NotificationClickNotifier>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt
similarity index 73%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt
index 33143fe..8d30049 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationPresenter by Kosmos.Fixture { mock<NotificationPresenter>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
similarity index 71%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
index 33143fe..554bdbe 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationRemoteInputManager by
+    Kosmos.Fixture { mock<NotificationRemoteInputManager>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt
similarity index 71%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt
index 33143fe..e8ca3b8 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationShadeWindowController by
+    Kosmos.Fixture { mock<NotificationShadeWindowController>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt
similarity index 66%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt
index 33143fe..c337ac2 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar.notification
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.statusBarNotificationActivityStarter
+
+var Kosmos.notificationActivityStarter: NotificationActivityStarter by
+    Kosmos.Fixture { statusBarNotificationActivityStarter }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt
similarity index 68%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt
index 33143fe..c3db34b 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar.notification
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationLaunchAnimatorControllerProvider by
+    Kosmos.Fixture { mock<NotificationLaunchAnimatorControllerProvider>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt
similarity index 68%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt
index 33143fe..1f45fbb 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar.notification.collection
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.notifLiveDataStore: NotifLiveDataStore by
+    Kosmos.Fixture { NotifLiveDataStoreImpl(fakeExecutor) }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt
similarity index 69%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt
index 33143fe..358d251 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar.notification.collection.coordinator
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.visualStabilityCoordinator by Kosmos.Fixture { mock<VisualStabilityCoordinator>() }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt
similarity index 73%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt
index 33143fe..a5c9561 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar.notification.collection.provider
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.launchFullScreenIntentProvider by Kosmos.Fixture { LaunchFullScreenIntentProvider() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt
new file mode 100644
index 0000000..edce5d58
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.render
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.collection.notifLiveDataStore
+import com.android.systemui.statusbar.notification.collection.notifPipeline
+import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+
+var Kosmos.notificationVisibilityProvider: NotificationVisibilityProvider by
+    Kosmos.Fixture {
+        NotificationVisibilityProviderImpl(
+            activeNotificationsInteractor,
+            notifLiveDataStore,
+            notifPipeline,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt
new file mode 100644
index 0000000..1e3897b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.statusbar.notification.collection.coordinator.visualStabilityCoordinator
+import com.android.systemui.statusbar.notification.collection.inflation.OnUserInteractionCallbackImpl
+import com.android.systemui.statusbar.notification.collection.notifCollection
+import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider
+import com.android.systemui.statusbar.policy.headsUpManager
+
+var Kosmos.onUserInteractionCallback: OnUserInteractionCallback by
+    Kosmos.Fixture {
+        OnUserInteractionCallbackImpl(
+            notificationVisibilityProvider,
+            notifCollection,
+            headsUpManager,
+            statusBarStateController,
+            visualStabilityCoordinator,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 5ef9a8e..db40509 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -16,8 +16,11 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
 import com.android.systemui.kosmos.Kosmos
@@ -33,7 +36,10 @@
         keyguardInteractor = keyguardInteractor,
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         shadeInteractor = shadeInteractor,
+        communalInteractor = communalInteractor,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
         lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
+        glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
+        lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
new file mode 100644
index 0000000..6ddc9df
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.app.keyguardManager
+import android.content.applicationContext
+import android.os.fakeExecutorHandler
+import android.service.dream.dreamManager
+import com.android.internal.logging.metricsLogger
+import com.android.internal.widget.lockPatternUtils
+import com.android.systemui.activityIntentHelper
+import com.android.systemui.animation.activityLaunchAnimator
+import com.android.systemui.assist.assistManager
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.settings.userTracker
+import com.android.systemui.shade.domain.interactor.shadeAnimationInteractor
+import com.android.systemui.shade.shadeController
+import com.android.systemui.shade.shadeViewController
+import com.android.systemui.statusbar.notification.collection.provider.launchFullScreenIntentProvider
+import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider
+import com.android.systemui.statusbar.notification.notificationLaunchAnimatorControllerProvider
+import com.android.systemui.statusbar.notification.row.onUserInteractionCallback
+import com.android.systemui.statusbar.notificationClickNotifier
+import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.statusbar.notificationPresenter
+import com.android.systemui.statusbar.notificationRemoteInputManager
+import com.android.systemui.statusbar.notificationShadeWindowController
+import com.android.systemui.statusbar.policy.headsUpManager
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.android.systemui.wmshell.bubblesManager
+import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.statusBarNotificationActivityStarter by
+    Kosmos.Fixture {
+        StatusBarNotificationActivityStarter(
+            applicationContext,
+            applicationContext.displayId,
+            fakeExecutorHandler,
+            fakeExecutor,
+            notificationVisibilityProvider,
+            headsUpManager,
+            activityStarter,
+            notificationClickNotifier,
+            statusBarKeyguardViewManager,
+            keyguardManager,
+            dreamManager,
+            Optional.of(bubblesManager),
+            { assistManager },
+            notificationRemoteInputManager,
+            notificationLockscreenUserManager,
+            shadeController,
+            keyguardStateController,
+            lockPatternUtils,
+            statusBarRemoteInputCallback,
+            activityIntentHelper,
+            metricsLogger,
+            statusBarNotificationActivityStarterLogger,
+            onUserInteractionCallback,
+            notificationPresenter,
+            shadeViewController,
+            notificationShadeWindowController,
+            activityLaunchAnimator,
+            shadeAnimationInteractor,
+            notificationLaunchAnimatorControllerProvider,
+            launchFullScreenIntentProvider,
+            powerInteractor,
+            userTracker,
+        )
+    }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt
similarity index 68%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt
index 33143fe..31cfc97 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar.phone
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.statusBarNotificationActivityStarterLogger by
+    Kosmos.Fixture { StatusBarNotificationActivityStarterLogger(logcatLogBuffer()) }
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt
similarity index 72%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt
index 33143fe..475d7fa 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar.phone
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.statusBarRemoteInputCallback by Kosmos.Fixture { mock<StatusBarRemoteInputCallback>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index a9c8ec7..32d572e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -35,6 +35,7 @@
     override val isRoaming = MutableStateFlow(false)
     override val operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null)
     override val isInService = MutableStateFlow(false)
+    override val isNonTerrestrial = MutableStateFlow(false)
     override val isGsm = MutableStateFlow(false)
     override val cdmaLevel = MutableStateFlow(0)
     override val primaryLevel = MutableStateFlow(0)
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
similarity index 72%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
index 33143fe..0e909c4 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.statusbar.policy
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.keyguardStateController by Kosmos.Fixture { mock<KeyguardStateController>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt
index e208add..5476d55 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt
@@ -17,7 +17,15 @@
 package com.android.systemui.statusbar.ui
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.statusbar.policy.configurationController
 
-val Kosmos.systemBarUtilsState by
-    Kosmos.Fixture { SystemBarUtilsState(configurationController, systemBarUtilsProxy) }
+val Kosmos.systemBarUtilsState by Fixture {
+    SystemBarUtilsState(
+        testDispatcher,
+        testDispatcher,
+        configurationController,
+        systemBarUtilsProxy,
+    )
+}
diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt
similarity index 75%
copy from nfc/java/android/nfc/WlcLDeviceInfo.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt
index 33143fe..1d05d62 100644
--- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
-package android.nfc;
+package com.android.systemui.wmshell
 
-parcelable WlcLDeviceInfo;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.bubblesManager by Kosmos.Fixture { mock<BubblesManager>() }
diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..be1f081
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2023, 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.
+ */
+-->
+<resources>
+    <!-- If true, attach the navigation bar to the app during app transition -->
+    <bool name="config_attachNavBarToAppDuringTransition">false</bool>
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..be1f081
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2023, 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.
+ */
+-->
+<resources>
+    <!-- If true, attach the navigation bar to the app during app transition -->
+    <bool name="config_attachNavBarToAppDuringTransition">false</bool>
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..be1f081
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2023, 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.
+ */
+-->
+<resources>
+    <!-- If true, attach the navigation bar to the app during app transition -->
+    <bool name="config_attachNavBarToAppDuringTransition">false</bool>
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..be1f081
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2023, 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.
+ */
+-->
+<resources>
+    <!-- If true, attach the navigation bar to the app during app transition -->
+    <bool name="config_attachNavBarToAppDuringTransition">false</bool>
+</resources>
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 3d8d7b7..d656892 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -4413,25 +4413,50 @@
     @Override
     public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) {
         mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY);
+        final ComponentName componentName = info.getComponentName();
 
         // Warning is not required if the service is already enabled.
         synchronized (mLock) {
             final AccessibilityUserState userState = getCurrentUserStateLocked();
-            if (userState.getEnabledServicesLocked().contains(info.getComponentName())) {
+            if (userState.getEnabledServicesLocked().contains(componentName)) {
                 return false;
             }
         }
         // Warning is not required if the service is already assigned to a shortcut.
         for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) {
             if (getAccessibilityShortcutTargets(shortcutType).contains(
-                    info.getComponentName().flattenToString())) {
+                    componentName.flattenToString())) {
                 return false;
             }
         }
+        // Warning is not required if the service is preinstalled and in the
+        // trustedAccessibilityServices allowlist.
+        if (android.view.accessibility.Flags.skipAccessibilityWarningDialogForTrustedServices()
+                && isAccessibilityServicePreinstalledAndTrusted(info)) {
+            return false;
+        }
+
         // Warning is required by default.
         return true;
     }
 
+    private boolean isAccessibilityServicePreinstalledAndTrusted(AccessibilityServiceInfo info) {
+        final ComponentName componentName = info.getComponentName();
+        final boolean isPreinstalled =
+                info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp();
+        if (isPreinstalled) {
+            final String[] trustedAccessibilityServices =
+                    mContext.getResources().getStringArray(
+                            R.array.config_trustedAccessibilityServices);
+            if (Arrays.stream(trustedAccessibilityServices)
+                    .map(ComponentName::unflattenFromString)
+                    .anyMatch(componentName::equals)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @Override
     public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
diff --git a/services/autofill/bugfixes.aconfig b/services/autofill/bugfixes.aconfig
index b5130a1..ced10fb 100644
--- a/services/autofill/bugfixes.aconfig
+++ b/services/autofill/bugfixes.aconfig
@@ -34,3 +34,10 @@
   description: "Mitigation for autofill providers miscalculating view visibility"
   bug: "291795358"
 }
+
+flag {
+  name: "remote_fill_service_use_weak_reference"
+  namespace: "autofill"
+  description: "Use weak reference to address binder leak problem"
+  bug: "307972253"
+}
diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
index 6d0915b..5c93991 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
@@ -17,6 +17,7 @@
 package com.android.server.autofill;
 
 import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
+import static android.service.autofill.Flags.remoteFillServiceUseWeakReference;
 
 import static com.android.server.autofill.Helper.sVerbose;
 
@@ -44,6 +45,7 @@
 import com.android.internal.infra.ServiceConnector;
 import com.android.internal.os.IResultReceiver;
 
+import java.lang.ref.WeakReference;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
@@ -65,6 +67,8 @@
     private final Object mLock = new Object();
     private CompletableFuture<FillResponse> mPendingFillRequest;
     private int mPendingFillRequestId = INVALID_REQUEST_ID;
+    private AtomicReference<IFillCallback> mFillCallback;
+    private AtomicReference<ISaveCallback> mSaveCallback;
     private final ComponentName mComponentName;
 
     private final boolean mIsCredentialAutofillService;
@@ -150,6 +154,89 @@
         }
     }
 
+    static class IFillCallbackDelegate extends IFillCallback.Stub {
+        private WeakReference<IFillCallback> mCallbackWeakRef;
+
+        IFillCallbackDelegate(IFillCallback callback) {
+            mCallbackWeakRef = new WeakReference(callback);
+        }
+
+        @Override
+        public void onCancellable(ICancellationSignal cancellation) throws RemoteException {
+            IFillCallback callback = mCallbackWeakRef.get();
+            if (callback != null) {
+                callback.onCancellable(cancellation);
+            }
+        }
+
+        @Override
+        public void onSuccess(FillResponse response) throws RemoteException {
+            IFillCallback callback = mCallbackWeakRef.get();
+            if (callback != null) {
+                callback.onSuccess(response);
+            }
+        }
+
+        @Override
+        public void onFailure(int requestId, CharSequence message) throws RemoteException {
+            IFillCallback callback = mCallbackWeakRef.get();
+            if (callback != null) {
+                callback.onFailure(requestId, message);
+            }
+        }
+    }
+
+    static class ISaveCallbackDelegate extends ISaveCallback.Stub {
+
+        private WeakReference<ISaveCallback> mCallbackWeakRef;
+
+        ISaveCallbackDelegate(ISaveCallback callback) {
+            mCallbackWeakRef = new WeakReference(callback);
+        }
+
+        @Override
+        public void onSuccess(IntentSender intentSender) throws RemoteException {
+            ISaveCallback callback = mCallbackWeakRef.get();
+            if (callback != null) {
+                callback.onSuccess(intentSender);
+            }
+        }
+
+        @Override
+        public void onFailure(CharSequence message) throws RemoteException {
+            ISaveCallback callback = mCallbackWeakRef.get();
+            if (callback != null) {
+                callback.onFailure(message);
+            }
+        }
+    }
+
+    /**
+     * Wraps an {@link IFillCallback} object using weak reference.
+     *
+     * This prevents lingering allocation issue by breaking the chain of strong references from
+     * Binder to {@link callback}. Since {@link callback} is not held by Binder anymore, it needs
+     * to be held by {@link mFillCallback} so it's not deallocated prematurely.
+     */
+    private IFillCallback maybeWrapWithWeakReference(IFillCallback callback) {
+        if (remoteFillServiceUseWeakReference()) {
+            mFillCallback = new AtomicReference<>(callback);
+            return new IFillCallbackDelegate(callback);
+        }
+        return callback;
+    }
+
+    /**
+     * Wraps an {@link ISaveCallback} object using weak reference.
+     */
+    private ISaveCallback maybeWrapWithWeakReference(ISaveCallback callback) {
+        if (remoteFillServiceUseWeakReference()) {
+            mSaveCallback = new AtomicReference<>(callback);
+            return new ISaveCallbackDelegate(callback);
+        }
+        return callback;
+    }
+
     public void onFillCredentialRequest(@NonNull FillRequest request,
             IAutoFillManagerClient autofillCallback) {
         if (sVerbose) {
@@ -164,29 +251,30 @@
             }
 
             CompletableFuture<FillResponse> fillRequest = new CompletableFuture<>();
-            remoteService.onFillCredentialRequest(request, new IFillCallback.Stub() {
-                @Override
-                public void onCancellable(ICancellationSignal cancellation) {
-                    CompletableFuture<FillResponse> future = futureRef.get();
-                    if (future != null && future.isCancelled()) {
-                        dispatchCancellationSignal(cancellation);
-                    } else {
-                        cancellationSink.set(cancellation);
-                    }
-                }
+            remoteService.onFillCredentialRequest(
+                    request, maybeWrapWithWeakReference(new IFillCallback.Stub() {
+                        @Override
+                        public void onCancellable(ICancellationSignal cancellation) {
+                            CompletableFuture<FillResponse> future = futureRef.get();
+                            if (future != null && future.isCancelled()) {
+                                dispatchCancellationSignal(cancellation);
+                            } else {
+                                cancellationSink.set(cancellation);
+                            }
+                        }
 
-                @Override
-                public void onSuccess(FillResponse response) {
-                    fillRequest.complete(response);
-                }
+                        @Override
+                        public void onSuccess(FillResponse response) {
+                            fillRequest.complete(response);
+                        }
 
-                @Override
-                public void onFailure(int requestId, CharSequence message) {
-                    String errorMessage = message == null ? "" : String.valueOf(message);
-                    fillRequest.completeExceptionally(
-                            new RuntimeException(errorMessage));
-                }
-            }, autofillCallback);
+                        @Override
+                        public void onFailure(int requestId, CharSequence message) {
+                            String errorMessage = message == null ? "" : String.valueOf(message);
+                            fillRequest.completeExceptionally(
+                                    new RuntimeException(errorMessage));
+                        }
+                    }), autofillCallback);
             return fillRequest;
         }).orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
         futureRef.set(connectThenFillRequest);
@@ -235,29 +323,30 @@
             }
 
             CompletableFuture<FillResponse> fillRequest = new CompletableFuture<>();
-            remoteService.onFillRequest(request, new IFillCallback.Stub() {
-                @Override
-                public void onCancellable(ICancellationSignal cancellation) {
-                    CompletableFuture<FillResponse> future = futureRef.get();
-                    if (future != null && future.isCancelled()) {
-                        dispatchCancellationSignal(cancellation);
-                    } else {
-                        cancellationSink.set(cancellation);
-                    }
-                }
+            remoteService.onFillRequest(
+                    request, maybeWrapWithWeakReference(new IFillCallback.Stub() {
+                        @Override
+                        public void onCancellable(ICancellationSignal cancellation) {
+                            CompletableFuture<FillResponse> future = futureRef.get();
+                            if (future != null && future.isCancelled()) {
+                                dispatchCancellationSignal(cancellation);
+                            } else {
+                                cancellationSink.set(cancellation);
+                            }
+                        }
 
-                @Override
-                public void onSuccess(FillResponse response) {
-                    fillRequest.complete(response);
-                }
+                        @Override
+                        public void onSuccess(FillResponse response) {
+                            fillRequest.complete(response);
+                        }
 
-                @Override
-                public void onFailure(int requestId, CharSequence message) {
-                    String errorMessage = message == null ? "" : String.valueOf(message);
-                    fillRequest.completeExceptionally(
-                            new RuntimeException(errorMessage));
-                }
-            });
+                        @Override
+                        public void onFailure(int requestId, CharSequence message) {
+                            String errorMessage = message == null ? "" : String.valueOf(message);
+                            fillRequest.completeExceptionally(
+                                    new RuntimeException(errorMessage));
+                        }
+                    }));
             return fillRequest;
         }).orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
         futureRef.set(connectThenFillRequest);
@@ -294,7 +383,7 @@
             if (sVerbose) Slog.v(TAG, "calling onSaveRequest()");
 
             CompletableFuture<IntentSender> save = new CompletableFuture<>();
-            service.onSaveRequest(request, new ISaveCallback.Stub() {
+            service.onSaveRequest(request, maybeWrapWithWeakReference(new ISaveCallback.Stub() {
                 @Override
                 public void onSuccess(IntentSender intentSender) {
                     save.complete(intentSender);
@@ -304,7 +393,7 @@
                 public void onFailure(CharSequence message) {
                     save.completeExceptionally(new RuntimeException(String.valueOf(message)));
                 }
-            });
+            }));
             return save;
         }).orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS)
                 .whenComplete((res, err) -> Handler.getMain().post(() -> {
diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
index 42ab05f..4d42f15 100644
--- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
@@ -58,11 +58,13 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.autofill.AutofillManager;
 import android.widget.ImageView;
 import android.widget.RemoteViews;
+import android.widget.ScrollView;
 import android.widget.TextView;
 
 import com.android.internal.R;
@@ -370,9 +372,23 @@
         params.windowAnimations = R.style.AutofillSaveAnimation;
         params.setTrustedOverlay();
 
+        ScrollView scrollView = view.findViewById(R.id.autofill_sheet_scroll_view);
+
+        View divider = view.findViewById(R.id.autofill_sheet_divider);
+
+        ViewTreeObserver observer = scrollView.getViewTreeObserver();
+        observer.addOnGlobalLayoutListener(() -> adjustDividerVisibility(scrollView, divider));
+
+        scrollView.getViewTreeObserver()
+                .addOnScrollChangedListener(() -> adjustDividerVisibility(scrollView, divider));
         show();
     }
 
+    private void adjustDividerVisibility(ScrollView scrollView, View divider) {
+        boolean canScrollDown = scrollView.canScrollVertically(1); // 1 to check scrolling down
+        divider.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE);
+    }
+
     private boolean applyCustomDescription(@NonNull Context context, @NonNull View saveUiView,
             @NonNull ValueFinder valueFinder, @NonNull SaveInfo info) {
         final CustomDescription customDescription = info.getCustomDescription();
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index 549fa36..4022e33 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -10,6 +10,15 @@
 }
 
 flag {
+    name: "enable_metrics_system_backup_agents"
+    namespace: "backup"
+    description: "Enable SystemBackupAgent to collect B&R agent metrics by passing an instance of "
+            "the logger to each BackupHelper."
+    bug: "296844513"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "enable_max_size_writes_to_pipes"
     namespace: "onboarding"
     description: "Enables the write buffer to pipes to be of maximum size."
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 50e1862..84e1d90 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -247,6 +247,11 @@
         final Context context = getContext();
 
         mPersistentStore = new PersistentDataStore();
+        mAssociationRequestsProcessor = new AssociationRequestsProcessor(
+                /* cdmService */ this, mAssociationStore);
+        mBackupRestoreProcessor = new BackupRestoreProcessor(
+                /* cdmService */ this, mAssociationStore, mPersistentStore,
+                mSystemDataTransferRequestStore, mAssociationRequestsProcessor);
 
         loadAssociationsFromDisk();
         mAssociationStore.registerListener(mAssociationStoreChangeListener);
@@ -254,17 +259,12 @@
         mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(mUserManager,
                 mAssociationStore, mDevicePresenceCallback);
 
-        mAssociationRequestsProcessor = new AssociationRequestsProcessor(
-                /* cdmService */this, mAssociationStore);
         mCompanionAppController = new CompanionApplicationController(
                 context, mAssociationStore, mDevicePresenceMonitor);
         mTransportManager = new CompanionTransportManager(context, mAssociationStore);
         mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
                 mPackageManagerInternal, mAssociationStore,
                 mSystemDataTransferRequestStore, mTransportManager);
-        mBackupRestoreProcessor = new BackupRestoreProcessor(
-                /* cdmService */ this, mAssociationStore, mPersistentStore,
-                mSystemDataTransferRequestStore, mAssociationRequestsProcessor);
         // TODO(b/279663946): move context sync to a dedicated system service
         mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager);
 
diff --git a/services/core/Android.bp b/services/core/Android.bp
index fdcd27d..3164e08 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -211,6 +211,7 @@
         "com_android_wm_shell_flags_lib",
         "com.android.server.utils_aconfig-java",
         "service-jobscheduler-deviceidle.flags-aconfig-java",
+        "backup_flags_lib",
         "policy_flags_lib",
     ],
     javac_shard_size: 50,
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 3483c1a..a493d7a 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -95,6 +95,7 @@
     private static final int ALLOW_OVERRIDE_APP_RESTRICTIONS = 0x100;
     private static final int ALLOW_IMPLICIT_BROADCASTS = 0x200;
     private static final int ALLOW_VENDOR_APEX = 0x400;
+    private static final int ALLOW_SIGNATURE_PERMISSIONS = 0x800;
     private static final int ALLOW_ALL = ~0;
 
     // property for runtime configuration differentiation
@@ -597,7 +598,7 @@
 
         // Vendors are only allowed to customize these
         int vendorPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PRIVAPP_PERMISSIONS
-                | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX;
+                | ALLOW_SIGNATURE_PERMISSIONS | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX;
         if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.O_MR1) {
             // For backward compatibility
             vendorPermissionFlag |= (ALLOW_PERMISSIONS | ALLOW_APP_CONFIGS);
@@ -649,9 +650,9 @@
         // TODO(b/157203468): ALLOW_HIDDENAPI_WHITELISTING must be removed because we prohibited
         // the use of hidden APIs from the product partition.
         int productPermissionFlag = ALLOW_FEATURES | ALLOW_LIBS | ALLOW_PERMISSIONS
-                | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_HIDDENAPI_WHITELISTING
-                | ALLOW_ASSOCIATIONS | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS
-                | ALLOW_VENDOR_APEX;
+                | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_SIGNATURE_PERMISSIONS
+                | ALLOW_HIDDENAPI_WHITELISTING | ALLOW_ASSOCIATIONS
+                | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS | ALLOW_VENDOR_APEX;
         if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.R) {
             // TODO(b/157393157): This must check product interface enforcement instead of
             // DEVICE_INITIAL_SDK_INT for the devices without product interface enforcement.
@@ -772,6 +773,8 @@
             final boolean allowAppConfigs = (permissionFlag & ALLOW_APP_CONFIGS) != 0;
             final boolean allowPrivappPermissions = (permissionFlag & ALLOW_PRIVAPP_PERMISSIONS)
                     != 0;
+            final boolean allowSignaturePermissions = (permissionFlag & ALLOW_SIGNATURE_PERMISSIONS)
+                    != 0;
             final boolean allowOemPermissions = (permissionFlag & ALLOW_OEM_PERMISSIONS) != 0;
             final boolean allowApiWhitelisting = (permissionFlag & ALLOW_HIDDENAPI_WHITELISTING)
                     != 0;
@@ -1246,6 +1249,38 @@
                             XmlUtils.skipCurrentTag(parser);
                         }
                     } break;
+                    case "signature-permissions": {
+                        if (allowSignaturePermissions) {
+                            // signature permissions from system, apex, vendor, product and
+                            // system_ext partitions are stored separately. This is to
+                            // prevent xml files in the vendor partition from granting
+                            // permissions to signature apps in the system partition and vice versa.
+                            boolean vendor = permFile.toPath().startsWith(
+                                    Environment.getVendorDirectory().toPath() + "/")
+                                    || permFile.toPath().startsWith(
+                                    Environment.getOdmDirectory().toPath() + "/");
+                            boolean product = permFile.toPath().startsWith(
+                                    Environment.getProductDirectory().toPath() + "/");
+                            boolean systemExt = permFile.toPath().startsWith(
+                                    Environment.getSystemExtDirectory().toPath() + "/");
+                            if (vendor) {
+                                readSignatureAppPermissions(parser,
+                                        mPermissionAllowlist.getVendorSignatureAppAllowlist());
+                            } else if (product) {
+                                readSignatureAppPermissions(parser,
+                                        mPermissionAllowlist.getProductSignatureAppAllowlist());
+                            } else if (systemExt) {
+                                readSignatureAppPermissions(parser,
+                                        mPermissionAllowlist.getSystemExtSignatureAppAllowlist());
+                            } else {
+                                readSignatureAppPermissions(parser,
+                                        mPermissionAllowlist.getSignatureAppAllowlist());
+                            }
+                        } else {
+                            logNotAllowedInPartition(name, permFile, parser);
+                            XmlUtils.skipCurrentTag(parser);
+                        }
+                    } break;
                     case "oem-permissions": {
                         if (allowOemPermissions) {
                             readOemPermissions(parser);
@@ -1655,6 +1690,12 @@
         readPermissionAllowlist(parser, allowlist, "privapp-permissions");
     }
 
+    private void readSignatureAppPermissions(@NonNull XmlPullParser parser,
+            @NonNull ArrayMap<String, ArrayMap<String, Boolean>> allowlist)
+            throws IOException, XmlPullParserException {
+        readPermissionAllowlist(parser, allowlist, "signature-permissions");
+    }
+
     private void readInstallInUserType(XmlPullParser parser,
             Map<String, Set<String>> doInstallMap,
             Map<String, Set<String>> nonInstallMap)
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c9bd0b4..86894fd 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4830,11 +4830,7 @@
             if (!mConstants.mEnableWaitForFinishAttachApplication) {
                 finishAttachApplicationInner(startSeq, callingUid, pid);
             }
-
-            // Temporarily disable sending BOOT_COMPLETED to see if this was impacting perf tests
-            if (false) {
-                maybeSendBootCompletedLocked(app);
-            }
+            maybeSendBootCompletedLocked(app);
         } catch (Exception e) {
             // We need kill the process group here. (b/148588589)
             Slog.wtf(TAG, "Exception thrown during bind of " + app, e);
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index c96c2ff..3487ae3 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -151,6 +151,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
@@ -653,7 +654,7 @@
             try {
                 future.get();
                 return;
-            } catch (ExecutionException e) {
+            } catch (ExecutionException | CancellationException e) {
                 return;
             } catch (InterruptedException e) {
                 // Keep looping
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 2cac7a0..db0f03f 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -1244,9 +1244,11 @@
     }
 
     private void deliveryTimeout(@NonNull BroadcastProcessQueue queue) {
+        final int cookie = traceBegin("deliveryTimeout");
         synchronized (mService) {
             deliveryTimeoutLocked(queue);
         }
+        traceEnd(cookie);
     }
 
     private void deliveryTimeoutLocked(@NonNull BroadcastProcessQueue queue) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 4cbee2b..9f7c07e 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -18,9 +18,6 @@
 
 import static android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED;
 import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT;
-import static android.media.audio.Flags.autoPublicVolumeApiHardening;
-import static android.media.audio.Flags.automaticBtDeviceType;
-import static android.media.audio.Flags.focusFreezeTestApi;
 import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET;
 import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER;
 import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP;
@@ -33,6 +30,9 @@
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
 import static android.media.AudioManager.STREAM_SYSTEM;
+import static android.media.audio.Flags.autoPublicVolumeApiHardening;
+import static android.media.audio.Flags.automaticBtDeviceType;
+import static android.media.audio.Flags.focusFreezeTestApi;
 import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
 import static android.os.Process.FIRST_APPLICATION_UID;
 import static android.os.Process.INVALID_UID;
@@ -116,7 +116,6 @@
 import android.media.AudioRecordingConfiguration;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
-import android.media.AudioTrack;
 import android.media.BluetoothProfileConnectionInfo;
 import android.media.FadeManagerConfiguration;
 import android.media.IAudioDeviceVolumeDispatcher;
@@ -144,7 +143,7 @@
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.IStreamAliasingDispatcher;
 import android.media.IVolumeController;
-import android.media.LoudnessCodecConfigurator;
+import android.media.LoudnessCodecController;
 import android.media.LoudnessCodecInfo;
 import android.media.MediaCodec;
 import android.media.MediaMetrics;
@@ -10737,34 +10736,35 @@
         mLoudnessCodecHelper.unregisterLoudnessCodecUpdatesDispatcher(dispatcher);
     }
 
-    /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */
+    /** @see LoudnessCodecController#create(int) */
     @Override
-    public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
-        mLoudnessCodecHelper.startLoudnessCodecUpdates(piid, codecInfoList);
+    public void startLoudnessCodecUpdates(int sessionId) {
+        mLoudnessCodecHelper.startLoudnessCodecUpdates(sessionId);
     }
 
-    /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */
+    /** @see LoudnessCodecController#release() */
     @Override
-    public void stopLoudnessCodecUpdates(int piid) {
-        mLoudnessCodecHelper.stopLoudnessCodecUpdates(piid);
+    public void stopLoudnessCodecUpdates(int sessionId) {
+        mLoudnessCodecHelper.stopLoudnessCodecUpdates(sessionId);
     }
 
-    /** @see LoudnessCodecConfigurator#addMediaCodec(MediaCodec) */
+    /** @see LoudnessCodecController#addMediaCodec(MediaCodec) */
     @Override
-    public void addLoudnessCodecInfo(int piid, int mediaCodecHash, LoudnessCodecInfo codecInfo) {
-        mLoudnessCodecHelper.addLoudnessCodecInfo(piid, mediaCodecHash, codecInfo);
+    public void addLoudnessCodecInfo(int sessionId, int mediaCodecHash,
+            LoudnessCodecInfo codecInfo) {
+        mLoudnessCodecHelper.addLoudnessCodecInfo(sessionId, mediaCodecHash, codecInfo);
     }
 
-    /** @see LoudnessCodecConfigurator#removeMediaCodec(MediaCodec) */
+    /** @see LoudnessCodecController#removeMediaCodec(MediaCodec) */
     @Override
-    public void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
-        mLoudnessCodecHelper.removeLoudnessCodecInfo(piid, codecInfo);
+    public void removeLoudnessCodecInfo(int sessionId, LoudnessCodecInfo codecInfo) {
+        mLoudnessCodecHelper.removeLoudnessCodecInfo(sessionId, codecInfo);
     }
 
-    /** @see LoudnessCodecConfigurator#getLoudnessCodecParams(AudioTrack, MediaCodec) */
+    /** @see LoudnessCodecController#getLoudnessCodecParams(MediaCodec) */
     @Override
-    public PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) {
-        return mLoudnessCodecHelper.getLoudnessParams(piid, codecInfo);
+    public PersistableBundle getLoudnessParams(LoudnessCodecInfo codecInfo) {
+        return mLoudnessCodecHelper.getLoudnessParams(codecInfo);
     }
 
     //==========================================================================================
diff --git a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
index 9b0afc4..01f770b 100644
--- a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
+++ b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
@@ -30,6 +30,8 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager.AudioDeviceCategory;
 import android.media.AudioPlaybackConfiguration;
@@ -44,7 +46,6 @@
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.util.Log;
-import android.util.SparseArray;
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
@@ -59,7 +60,9 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -75,9 +78,9 @@
     /**
      * Property containing a string to set for a custom built in speaker SPL range as defined by
      * CTA2075. The options that can be set are:
-     *   - "small": for max SPL with test signal < 75 dB,
-     *   - "medium": for max SPL with test signal between 70 and 90 dB,
-     *   - "large": for max SPL with test signal > 85 dB.
+     * - "small": for max SPL with test signal < 75 dB,
+     * - "medium": for max SPL with test signal between 70 and 90 dB,
+     * - "large": for max SPL with test signal > 85 dB.
      */
     private static final String SYSTEM_PROPERTY_SPEAKER_SPL_RANGE_SIZE =
             "audio.loudness.builtin-speaker-spl-range-size";
@@ -99,11 +102,13 @@
             SPL_RANGE_LARGE
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface DeviceSplRange {}
+    public @interface DeviceSplRange {
+    }
 
     private static final class LoudnessRemoteCallbackList extends
             RemoteCallbackList<ILoudnessCodecUpdatesDispatcher> {
         private final LoudnessCodecHelper mLoudnessCodecHelper;
+
         LoudnessRemoteCallbackList(LoudnessCodecHelper loudnessCodecHelper) {
             mLoudnessCodecHelper = loudnessCodecHelper;
         }
@@ -133,9 +138,15 @@
 
     private final Object mLock = new Object();
 
-    /** Contains for each started piid the set corresponding to unique registered audio codecs. */
+    /** Contains for each started track id the known started piids. */
     @GuardedBy("mLock")
-    private final SparseArray<Set<LoudnessCodecInfo>> mStartedPiids = new SparseArray<>();
+    private final HashMap<LoudnessTrackId, Set<Integer>> mStartedConfigPiids =
+            new HashMap<>();
+
+    /** Contains for each LoudnessTrackId a set of started coudec infos. */
+    @GuardedBy("mLock")
+    private final HashMap<LoudnessTrackId, Set<LoudnessCodecInfo>> mStartedConfigInfo =
+            new HashMap<>();
 
     /** Contains the current device id assignment for each piid. */
     @GuardedBy("mLock")
@@ -169,10 +180,12 @@
                 mMetadataType = metadataType;
                 return this;
             }
+
             Builder setIsDownmixing(boolean isDownmixing) {
                 mIsDownmixing = isDownmixing;
                 return this;
             }
+
             Builder setDeviceSplRange(@DeviceSplRange int deviceSplRange) {
                 mDeviceSplRange = deviceSplRange;
                 return this;
@@ -185,8 +198,8 @@
         }
 
         private LoudnessCodecInputProperties(int metadataType,
-                                             boolean isDownmixing,
-                                             @DeviceSplRange int deviceSplRange) {
+                boolean isDownmixing,
+                @DeviceSplRange int deviceSplRange) {
             mMetadataType = metadataType;
             mIsDownmixing = isDownmixing;
             mDeviceSplRange = deviceSplRange;
@@ -273,6 +286,50 @@
         }
     }
 
+    /**
+     * Contains the properties necessary to identify the tracks that are receiving annotated
+     * loudness data.
+     **/
+    @VisibleForTesting
+    static final class LoudnessTrackId {
+        private final int mSessionId;
+
+        private final int mPid;
+
+        private LoudnessTrackId(int sessionId, int pid) {
+            mSessionId = sessionId;
+            mPid = pid;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            // type check and cast
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            final LoudnessTrackId lti = (LoudnessTrackId) obj;
+            return mSessionId == lti.mSessionId && mPid == lti.mPid;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mSessionId, mPid);
+        }
+
+        @Override
+        public String toString() {
+            return "Loudness track id:"
+                    + " session ID: " + mSessionId
+                    + " pid: " + mPid;
+        }
+    }
+
     @GuardedBy("mLock")
     private final HashMap<LoudnessCodecInputProperties, PersistableBundle> mCachedProperties =
             new HashMap<>();
@@ -290,121 +347,161 @@
         mLoudnessUpdateDispatchers.unregister(dispatcher);
     }
 
-    void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+    void startLoudnessCodecUpdates(int sessionId) {
+        int pid = Binder.getCallingPid();
         if (DEBUG) {
-            Log.d(TAG, "startLoudnessCodecUpdates: piid " + piid + " codecInfos " + codecInfoList);
+            Log.d(TAG,
+                    "startLoudnessCodecUpdates: sessionId " + sessionId + " pid " + pid);
         }
 
+        final LoudnessTrackId newConfig = new LoudnessTrackId(sessionId, pid);
+        HashSet<Integer> newPiids;
         synchronized (mLock) {
-            if (mStartedPiids.contains(piid)) {
-                Log.w(TAG, "Already started loudness updates for piid " + piid);
+            if (mStartedConfigInfo.containsKey(newConfig)) {
+                Log.w(TAG, "Already started loudness updates for config: " + newConfig);
                 return;
             }
-            Set<LoudnessCodecInfo> infoSet = new HashSet<>(codecInfoList);
-            mStartedPiids.put(piid, infoSet);
 
-            int pid = Binder.getCallingPid();
-            mPiidToPidCache.put(piid, pid);
-
-            sLogger.enqueue(LoudnessEvent.getStartPiid(piid, pid));
+            mStartedConfigInfo.put(newConfig, new HashSet<>());
+            newPiids = new HashSet<>();
+            mStartedConfigPiids.put(newConfig, newPiids);
         }
 
         try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
             mAudioService.getActivePlaybackConfigurations().stream().filter(
-                    conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent(
-                    this::updateCodecParametersForConfiguration);
-        }
-    }
-
-    void stopLoudnessCodecUpdates(int piid) {
-        if (DEBUG) {
-            Log.d(TAG, "stopLoudnessCodecUpdates: piid " + piid);
-        }
-
-        synchronized (mLock) {
-            if (!mStartedPiids.contains(piid)) {
-                Log.w(TAG, "Loudness updates are already stopped for piid " + piid);
-                return;
-            }
-            mStartedPiids.remove(piid);
-
-            sLogger.enqueue(LoudnessEvent.getStopPiid(piid, mPiidToPidCache.get(piid, -1)));
-            mPiidToDeviceIdCache.delete(piid);
-            mPiidToPidCache.delete(piid);
-        }
-    }
-
-    void addLoudnessCodecInfo(int piid, int mediaCodecHash, LoudnessCodecInfo info) {
-        if (DEBUG) {
-            Log.d(TAG, "addLoudnessCodecInfo: piid " + piid + " mcHash " + mediaCodecHash + " info "
-                    + info);
-        }
-
-        Set<LoudnessCodecInfo> infoSet;
-        synchronized (mLock) {
-            if (!mStartedPiids.contains(piid)) {
-                Log.w(TAG, "Cannot add new loudness info for stopped piid " + piid);
-                return;
-            }
-
-            infoSet = mStartedPiids.get(piid);
-            infoSet.add(info);
-        }
-
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            mAudioService.getActivePlaybackConfigurations().stream().filter(
-                    conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent(
-                            apc -> {
-                                final AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo();
-                                if (deviceInfo != null) {
-                                    PersistableBundle updateBundle = new PersistableBundle();
-                                    synchronized (mLock) {
-                                        updateBundle.putPersistableBundle(
-                                                Integer.toString(mediaCodecHash),
-                                                getCodecBundle_l(deviceInfo, info));
-                                    }
-                                    if (!updateBundle.isDefinitelyEmpty()) {
-                                        dispatchNewLoudnessParameters(piid, updateBundle);
-                                    }
+                    conf -> conf.getSessionId() == sessionId
+                            && conf.getClientPid() == pid).forEach(apc -> {
+                                int piid = apc.getPlayerInterfaceId();
+                                synchronized (mLock) {
+                                    newPiids.add(piid);
+                                    mPiidToPidCache.put(piid, pid);
+                                    sLogger.enqueue(LoudnessEvent.getStartPiid(piid, pid));
                                 }
                             });
         }
     }
 
-    void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+    void stopLoudnessCodecUpdates(int sessionId) {
+        int pid = Binder.getCallingPid();
         if (DEBUG) {
-            Log.d(TAG, "removeLoudnessCodecInfo: piid " + piid + " info " + codecInfo);
+            Log.d(TAG,
+                    "stopLoudnessCodecUpdates: sessionId " + sessionId + " pid " + pid);
         }
+
+        final LoudnessTrackId config = new LoudnessTrackId(sessionId, pid);
         synchronized (mLock) {
-            if (!mStartedPiids.contains(piid)) {
-                Log.w(TAG, "Cannot remove loudness info for stopped piid " + piid);
+            if (!mStartedConfigInfo.containsKey(config)) {
+                Log.w(TAG, "Loudness updates are already stopped config: " + config);
                 return;
             }
-            final Set<LoudnessCodecInfo> infoSet = mStartedPiids.get(piid);
-            infoSet.remove(codecInfo);
+
+            final Set<Integer> startedPiidSet = mStartedConfigPiids.get(config);
+            if (startedPiidSet == null) {
+                Log.e(TAG, "Loudness updates are already stopped config: " + config);
+                return;
+            }
+            for (Integer piid : startedPiidSet) {
+                sLogger.enqueue(LoudnessEvent.getStopPiid(piid, mPiidToPidCache.get(piid, -1)));
+                mPiidToDeviceIdCache.delete(piid);
+                mPiidToPidCache.delete(piid);
+            }
+            mStartedConfigPiids.remove(config);
+            mStartedConfigInfo.remove(config);
         }
     }
 
-    PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) {
+    void addLoudnessCodecInfo(int sessionId, int mediaCodecHash,
+            LoudnessCodecInfo info) {
+        int pid = Binder.getCallingPid();
         if (DEBUG) {
-            Log.d(TAG, "getLoudnessParams: piid " + piid + " codecInfo " + codecInfo);
+            Log.d(TAG, "addLoudnessCodecInfo: sessionId " + sessionId
+                    + " mcHash " + mediaCodecHash + " info " + info + " pid " + pid);
         }
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            final List<AudioPlaybackConfiguration> configs =
-                    mAudioService.getActivePlaybackConfigurations();
 
-            for (final AudioPlaybackConfiguration apc : configs) {
-                if (apc.getPlayerInterfaceId() == piid) {
-                    final AudioDeviceInfo info = apc.getAudioDeviceInfo();
-                    if (info == null) {
-                        Log.i(TAG, "Player with piid " + piid + " is not assigned any device");
-                        break;
-                    }
+        final LoudnessTrackId config = new LoudnessTrackId(sessionId, pid);
+        Set<LoudnessCodecInfo> infoSet;
+        Set<Integer> piids;
+        synchronized (mLock) {
+            if (!mStartedConfigInfo.containsKey(config) || !mStartedConfigPiids.containsKey(
+                    config)) {
+                Log.w(TAG, "Cannot add new loudness info for stopped config " + config);
+                return;
+            }
+
+            piids = mStartedConfigPiids.get(config);
+            infoSet = mStartedConfigInfo.get(config);
+            infoSet.add(info);
+        }
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            final PersistableBundle updateBundle = new PersistableBundle();
+            Optional<AudioPlaybackConfiguration> apc =
+                    mAudioService.getActivePlaybackConfigurations().stream().filter(
+                            conf -> conf.getSessionId() == sessionId
+                                    && conf.getClientPid() == pid).findFirst();
+            if (apc.isEmpty()) {
+                if (DEBUG) {
+                    Log.d(TAG,
+                            "No APCs found when adding loudness codec info. Using AudioAttributes"
+                                    + " routing for initial update");
+                }
+                updateBundle.putPersistableBundle(Integer.toString(mediaCodecHash),
+                        getLoudnessParams(info));
+            } else {
+                final AudioDeviceInfo deviceInfo = apc.get().getAudioDeviceInfo();
+                if (deviceInfo != null) {
                     synchronized (mLock) {
-                        return getCodecBundle_l(info, codecInfo);
+                        // found a piid that matches the configuration
+                        piids.add(apc.get().getPlayerInterfaceId());
+
+                        updateBundle.putPersistableBundle(
+                                Integer.toString(mediaCodecHash),
+                                getCodecBundle_l(deviceInfo.getInternalType(),
+                                        deviceInfo.getAddress(), info));
                     }
                 }
             }
+            if (!updateBundle.isDefinitelyEmpty()) {
+                dispatchNewLoudnessParameters(sessionId, updateBundle);
+            }
+        }
+    }
+
+    void removeLoudnessCodecInfo(int sessionId, LoudnessCodecInfo codecInfo) {
+        if (DEBUG) {
+            Log.d(TAG, "removeLoudnessCodecInfo: session ID" + sessionId + " info " + codecInfo);
+        }
+
+        int pid = Binder.getCallingPid();
+        final LoudnessTrackId config = new LoudnessTrackId(sessionId, pid);
+        synchronized (mLock) {
+            if (!mStartedConfigInfo.containsKey(config) || !mStartedConfigPiids.containsKey(
+                    config)) {
+                Log.w(TAG, "Cannot remove loudness info for stopped config " + config);
+                return;
+            }
+            final Set<LoudnessCodecInfo> codecInfos = mStartedConfigInfo.get(config);
+            if (!codecInfos.remove(codecInfo)) {
+                Log.w(TAG, "Could not find to remove codecInfo " + codecInfo);
+            }
+        }
+    }
+
+    PersistableBundle getLoudnessParams(LoudnessCodecInfo codecInfo) {
+        if (DEBUG) {
+            Log.d(TAG, "getLoudnessParams: codecInfo " + codecInfo);
+        }
+        final ArrayList<AudioDeviceAttributes> devicesForAttributes =
+                mAudioService.getDevicesForAttributesInt(new AudioAttributes.Builder()
+                        .setUsage(AudioAttributes.USAGE_MEDIA)
+                        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+                        .build(), /*forVolume=*/false);
+        if (!devicesForAttributes.isEmpty()) {
+            final AudioDeviceAttributes audioDeviceAttribute = devicesForAttributes.get(0);
+            synchronized (mLock) {
+                return getCodecBundle_l(audioDeviceAttribute.getInternalType(),
+                        audioDeviceAttribute.getAddress(), codecInfo);
+            }
         }
 
         // return empty Bundle
@@ -444,13 +541,21 @@
                     continue;
                 }
                 mPiidToDeviceIdCache.put(piid, deviceInfo.getId());
-                if (mStartedPiids.contains(piid)) {
+                final LoudnessTrackId config = new LoudnessTrackId(apc.getSessionId(),
+                        apc.getClientPid());
+                if (mStartedConfigInfo.containsKey(config) && mStartedConfigPiids.containsKey(
+                        config)) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Updating config: " + config + " with APC " + apc);
+                    }
                     updateApcList.add(apc);
+                    // update the started piid set
+                    mStartedConfigPiids.get(config).add(piid);
                 }
             }
         }
 
-        updateApcList.forEach(apc -> updateCodecParametersForConfiguration(apc));
+        updateApcList.forEach(this::updateCodecParametersForConfiguration);
     }
 
     /** Updates and dispatches the new loudness parameters for all its registered codecs. */
@@ -458,13 +563,18 @@
         // Registered clients
         pw.println("\nRegistered clients:\n");
         synchronized (mLock) {
-            for (int i = 0; i < mStartedPiids.size(); ++i) {
-                int piid = mStartedPiids.keyAt(i);
-                int pid = mPiidToPidCache.get(piid, -1);
-                final Set<LoudnessCodecInfo> codecInfos = mStartedPiids.get(piid);
-                pw.println(String.format("Player piid %d pid %d active codec types %s\n", piid,
-                        pid, codecInfos.stream().map(Object::toString).collect(
-                                Collectors.joining(", "))));
+            for (Map.Entry<LoudnessTrackId, Set<Integer>> entry : mStartedConfigPiids.entrySet()) {
+                for (Integer piid : entry.getValue()) {
+                    int pid = mPiidToPidCache.get(piid, -1);
+                    final Set<LoudnessCodecInfo> codecInfos = mStartedConfigInfo.get(
+                            entry.getKey());
+                    if (codecInfos != null) {
+                        pw.println(
+                                String.format("Player piid %d pid %d active codec types %s\n", piid,
+                                        pid, codecInfos.stream().map(Object::toString).collect(
+                                                Collectors.joining(", "))));
+                    }
+                }
             }
             pw.println();
         }
@@ -481,10 +591,12 @@
                     if (DEBUG) {
                         Log.d(TAG, "Removing piid  " + piid);
                     }
-                    mStartedPiids.delete(piid);
                     mPiidToDeviceIdCache.delete(piid);
                 }
             }
+
+            mStartedConfigPiids.entrySet().removeIf(entry -> entry.getKey().mPid == pid);
+            mStartedConfigInfo.entrySet().removeIf(entry -> entry.getKey().mPid == pid);
         }
     }
 
@@ -499,46 +611,55 @@
         }
 
         final PersistableBundle allBundles = new PersistableBundle();
-        final int piid = apc.getPlayerInterfaceId();
 
         synchronized (mLock) {
-            final Set<LoudnessCodecInfo> codecInfos = mStartedPiids.get(piid);
-
+            final LoudnessTrackId config = new LoudnessTrackId(apc.getSessionId(),
+                    apc.getClientPid());
+            final Set<LoudnessCodecInfo> codecInfos = mStartedConfigInfo.get(config);
             final AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo();
+
             if (codecInfos != null && deviceInfo != null) {
                 for (LoudnessCodecInfo info : codecInfos) {
-                    allBundles.putPersistableBundle(Integer.toString(info.hashCode()),
-                            getCodecBundle_l(deviceInfo, info));
+                    if (info != null) {
+                        allBundles.putPersistableBundle(Integer.toString(info.hashCode()),
+                                getCodecBundle_l(deviceInfo.getInternalType(),
+                                        deviceInfo.getAddress(), info));
+                    }
                 }
             }
         }
 
         if (!allBundles.isDefinitelyEmpty()) {
-            dispatchNewLoudnessParameters(piid, allBundles);
+            dispatchNewLoudnessParameters(apc.getSessionId(), allBundles);
         }
     }
 
-    private void dispatchNewLoudnessParameters(int piid, PersistableBundle bundle) {
+    private void dispatchNewLoudnessParameters(int sessionId,
+            PersistableBundle bundle) {
         if (DEBUG) {
-            Log.d(TAG, "dispatchNewLoudnessParameters: piid " + piid + " bundle: " + bundle);
+            Log.d(TAG,
+                    "dispatchNewLoudnessParameters: sessionId " + sessionId + " bundle: " + bundle);
         }
         final int nbDispatchers = mLoudnessUpdateDispatchers.beginBroadcast();
         for (int i = 0; i < nbDispatchers; ++i) {
             try {
                 mLoudnessUpdateDispatchers.getBroadcastItem(i)
-                        .dispatchLoudnessCodecParameterChange(piid, bundle);
+                        .dispatchLoudnessCodecParameterChange(sessionId, bundle);
             } catch (RemoteException e) {
-                Log.e(TAG, "Error dispatching for piid: " + piid + " bundle: " + bundle , e);
+                Log.e(TAG, "Error dispatching for sessionId " + sessionId + " bundle: " + bundle,
+                        e);
             }
         }
         mLoudnessUpdateDispatchers.finishBroadcast();
     }
 
     @GuardedBy("mLock")
-    private PersistableBundle getCodecBundle_l(AudioDeviceInfo deviceInfo,
-                                             LoudnessCodecInfo codecInfo) {
+    private PersistableBundle getCodecBundle_l(int internalDeviceType,
+            String address,
+            LoudnessCodecInfo codecInfo) {
         LoudnessCodecInputProperties.Builder builder = new LoudnessCodecInputProperties.Builder();
-        LoudnessCodecInputProperties prop = builder.setDeviceSplRange(getDeviceSplRange(deviceInfo))
+        LoudnessCodecInputProperties prop = builder.setDeviceSplRange(
+                        getDeviceSplRange(internalDeviceType, address))
                 .setIsDownmixing(codecInfo.isDownmixing)
                 .setMetadataType(codecInfo.metadataType)
                 .build();
@@ -552,14 +673,15 @@
     }
 
     @DeviceSplRange
-    private int getDeviceSplRange(AudioDeviceInfo deviceInfo) {
-        final int internalDeviceType = deviceInfo.getInternalType();
-        final @AudioDeviceCategory int deviceCategory;
-        if (automaticBtDeviceType()) {
-            deviceCategory = mAudioService.getBluetoothAudioDeviceCategory(deviceInfo.getAddress());
-        } else {
-            deviceCategory = mAudioService.getBluetoothAudioDeviceCategory_legacy(
-                    deviceInfo.getAddress(), AudioSystem.isBluetoothLeDevice(internalDeviceType));
+    private int getDeviceSplRange(int internalDeviceType, String address) {
+        @AudioDeviceCategory int deviceCategory;
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            if (automaticBtDeviceType()) {
+                deviceCategory = mAudioService.getBluetoothAudioDeviceCategory(address);
+            } else {
+                deviceCategory = mAudioService.getBluetoothAudioDeviceCategory_legacy(
+                        address, AudioSystem.isBluetoothLeDevice(internalDeviceType));
+            }
         }
         if (internalDeviceType == AudioSystem.DEVICE_OUT_SPEAKER) {
             final String splRange = SystemProperties.get(
@@ -595,20 +717,28 @@
 
     private static String splRangeToString(@DeviceSplRange int splRange) {
         switch (splRange) {
-            case SPL_RANGE_LARGE: return "large";
-            case SPL_RANGE_MEDIUM: return "medium";
-            case SPL_RANGE_SMALL: return "small";
-            default: return "unknown";
+            case SPL_RANGE_LARGE:
+                return "large";
+            case SPL_RANGE_MEDIUM:
+                return "medium";
+            case SPL_RANGE_SMALL:
+                return "small";
+            default:
+                return "unknown";
         }
     }
 
     @DeviceSplRange
     private static int stringToSplRange(String splRange) {
         switch (splRange) {
-            case "large": return SPL_RANGE_LARGE;
-            case "medium": return SPL_RANGE_MEDIUM;
-            case "small": return SPL_RANGE_SMALL;
-            default: return SPL_RANGE_UNKNOWN;
+            case "large":
+                return SPL_RANGE_LARGE;
+            case "medium":
+                return SPL_RANGE_MEDIUM;
+            case "small":
+                return SPL_RANGE_SMALL;
+            default:
+                return SPL_RANGE_UNKNOWN;
         }
     }
 }
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index de4979a..5b9469b 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -20,7 +20,8 @@
 import android.app.backup.BackupAgentHelper;
 import android.app.backup.BackupAnnotations.BackupDestination;
 import android.app.backup.BackupDataInput;
-import android.app.backup.BackupHelper;
+import android.app.backup.BackupHelperWithLogger;
+import android.app.backup.BackupRestoreEventLogger;
 import android.app.backup.FullBackup;
 import android.app.backup.FullBackupDataOutput;
 import android.app.backup.WallpaperBackupHelper;
@@ -33,9 +34,10 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Slog;
-
 import com.google.android.collect.Sets;
 
+import com.android.server.backup.Flags;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.Set;
@@ -107,10 +109,12 @@
 
     private int mUserId = UserHandle.USER_SYSTEM;
     private boolean mIsProfileUser = false;
+    private BackupRestoreEventLogger mLogger;
 
     @Override
     public void onCreate(UserHandle user, @BackupDestination int backupDestination) {
         super.onCreate(user, backupDestination);
+        mLogger = this.getBackupRestoreEventLogger();
 
         mUserId = user.getIdentifier();
         if (mUserId != UserHandle.USER_SYSTEM) {
@@ -209,9 +213,12 @@
         }
     }
 
-    private void addHelperIfEligibleForUser(String keyPrefix, BackupHelper helper) {
+    private void addHelperIfEligibleForUser(String keyPrefix, BackupHelperWithLogger helper) {
         if (isHelperEligibleForUser(keyPrefix)) {
             addHelper(keyPrefix, helper);
+            if (Flags.enableMetricsSystemBackupAgents()) {
+                helper.setLogger(mLogger);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContext.java b/services/core/java/com/android/server/biometrics/log/BiometricContext.java
index f8a9867..7f04628 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricContext.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricContext.java
@@ -87,10 +87,30 @@
      *
      * @param context context that will be modified when changed
      * @param consumer callback when the context is modified
+     *
+     * @deprecated instead use {@link BiometricContext#subscribe(OperationContextExt, Consumer,
+     *                                                           Consumer, AuthenticateOptions)}
+     * TODO (b/294161627): Delete this API once Flags.DE_HIDL is removed.
      */
+    @Deprecated
     void subscribe(@NonNull OperationContextExt context,
             @NonNull Consumer<OperationContext> consumer);
 
+    /**
+     * Subscribe to context changes and start the HAL operation.
+     *
+     * Note that this method only notifies for properties that are visible to the HAL.
+     *
+     * @param context               context that will be modified when changed
+     * @param startHalConsumer      callback to start HAL operation after subscription is done
+     * @param updateContextConsumer callback when the context is modified
+     * @param options               authentication options for updating the context
+     */
+    void subscribe(@NonNull OperationContextExt context,
+            @NonNull Consumer<OperationContext> startHalConsumer,
+            @NonNull Consumer<OperationContext> updateContextConsumer,
+            @Nullable AuthenticateOptions options);
+
     /** Unsubscribe from context changes. */
     void unsubscribe(@NonNull OperationContextExt context);
 
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
index 535b7b7..d8dfa60 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
@@ -238,6 +238,19 @@
     }
 
     @Override
+    public void subscribe(@NonNull OperationContextExt context,
+            @NonNull Consumer<OperationContext> startHalConsumer,
+            @NonNull Consumer<OperationContext> updateContextConsumer,
+            @Nullable AuthenticateOptions options) {
+        mSubscribers.put(updateContext(context, context.isCrypto()), updateContextConsumer);
+        if (options != null) {
+            startHalConsumer.accept(context.toAidlContext(options));
+        } else {
+            startHalConsumer.accept(context.toAidlContext());
+        }
+    }
+
+    @Override
     public void unsubscribe(@NonNull OperationContextExt context) {
         mSubscribers.remove(context);
     }
diff --git a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java
index 0045d44..da4e515 100644
--- a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java
+++ b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java
@@ -102,6 +102,24 @@
      * @return the underlying AIDL context
      */
     @NonNull
+    public OperationContext toAidlContext(@NonNull AuthenticateOptions options) {
+        if (options instanceof FaceAuthenticateOptions) {
+            return toAidlContext((FaceAuthenticateOptions) options);
+        }
+        if (options instanceof FingerprintAuthenticateOptions) {
+            return toAidlContext((FingerprintAuthenticateOptions) options);
+        }
+        throw new IllegalStateException("Authenticate options are invalid.");
+    }
+
+    /**
+     * Gets the subset of the context that can be shared with the HAL and updates
+     * it with the given options.
+     *
+     * @param options authenticate options
+     * @return the underlying AIDL context
+     */
+    @NonNull
     public OperationContext toAidlContext(@NonNull FaceAuthenticateOptions options) {
         mAidlContext.authenticateReason = AuthenticateReason
                 .faceAuthenticateReason(getAuthReason(options));
diff --git a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java
index 0f01510..b0649b9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java
@@ -22,6 +22,7 @@
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.os.IBinder;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.OperationContextExt;
@@ -93,6 +94,9 @@
     }
 
     protected OperationContextExt getOperationContext() {
+        if (Flags.deHidl()) {
+            return mOperationContext;
+        }
         return getBiometricContext().updateContext(mOperationContext, isCryptoOperation());
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index 470dc4b..22e399c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -37,6 +37,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -153,7 +154,11 @@
                         0 /* vendorCode */);
                 mCallback.onClientFinished(this, false /* success */);
             } else {
-                mCancellationSignal = doAuthenticate();
+                if (Flags.deHidl()) {
+                    startAuthenticate();
+                } else {
+                    mCancellationSignal = doAuthenticate();
+                }
             }
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when requesting auth", e);
@@ -182,6 +187,33 @@
         }
     }
 
+    private void startAuthenticate() throws RemoteException {
+        final AidlSession session = getFreshDaemon();
+
+        if (session.hasContextMethods()) {
+            final OperationContextExt opContext = getOperationContext();
+            getBiometricContext().subscribe(opContext, ctx -> {
+                try {
+                    mCancellationSignal = session.getSession().authenticateWithContext(
+                            mOperationId, ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Remote exception when requesting auth", e);
+                    onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE,
+                            0 /* vendorCode */);
+                    mCallback.onClientFinished(this, false /* success */);
+                }
+            }, ctx -> {
+                try {
+                    session.getSession().onContextChanged(ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to notify context changed", e);
+                }
+            }, getOptions());
+        } else {
+            mCancellationSignal = session.getSession().authenticate(mOperationId);
+        }
+    }
+
     @Override
     protected void stopHalOperation() {
         unsubscribeBiometricContext();
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java
index a529fb9..5ddddda 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java
@@ -29,6 +29,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.BiometricsProto;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.OperationContextExt;
@@ -111,7 +112,11 @@
         }
 
         try {
-            mCancellationSignal = doDetectInteraction();
+            if (Flags.deHidl()) {
+                startDetect();
+            } else {
+                mCancellationSignal = doDetectInteraction();
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when requesting face detect", e);
             mCallback.onClientFinished(this, false /* success */);
@@ -138,6 +143,30 @@
         }
     }
 
+    private void startDetect() throws RemoteException {
+        final AidlSession session = getFreshDaemon();
+
+        if (session.hasContextMethods()) {
+            final OperationContextExt opContext = getOperationContext();
+            getBiometricContext().subscribe(opContext, ctx -> {
+                try {
+                    mCancellationSignal = session.getSession().detectInteractionWithContext(ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Remote exception when requesting face detect", e);
+                    mCallback.onClientFinished(this, false /* success */);
+                }
+            }, ctx -> {
+                try {
+                    session.getSession().onContextChanged(ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to notify context changed", e);
+                }
+            }, mOptions);
+        } else {
+            mCancellationSignal = session.getSession().detectInteraction();
+        }
+    }
+
     @Override
     public void onInteractionDetected() {
         vibrateSuccess();
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
index 0af6e40..f5c4529 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
@@ -36,6 +36,7 @@
 import android.view.Surface;
 
 import com.android.internal.R;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
@@ -187,7 +188,11 @@
                 features[i] = featureList.get(i);
             }
 
-            mCancellationSignal = doEnroll(features);
+            if (Flags.deHidl()) {
+                startEnroll(features);
+            } else {
+                mCancellationSignal = doEnroll(features);
+            }
         } catch (RemoteException | IllegalArgumentException e) {
             Slog.e(TAG, "Exception when requesting enroll", e);
             onError(BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS, 0 /* vendorCode */);
@@ -231,6 +236,48 @@
         }
     }
 
+    private void startEnroll(byte[] features) throws RemoteException {
+        final AidlSession session = getFreshDaemon();
+        final HardwareAuthToken hat =
+                HardwareAuthTokenUtils.toHardwareAuthToken(mHardwareAuthToken);
+
+        if (session.hasContextMethods()) {
+            final OperationContextExt opContext = getOperationContext();
+            getBiometricContext().subscribe(opContext, ctx -> {
+                try {
+                    if (session.supportsFaceEnrollOptions()) {
+                        FaceEnrollOptions options = new FaceEnrollOptions();
+                        options.hardwareAuthToken = hat;
+                        options.enrollmentType = EnrollmentType.DEFAULT;
+                        options.features = features;
+                        options.nativeHandlePreview = null;
+                        options.context = ctx;
+                        options.surfacePreview = mPreviewSurface;
+                        mCancellationSignal = session.getSession().enrollWithOptions(options);
+                    } else {
+                        mCancellationSignal = session.getSession().enrollWithContext(
+                                hat, EnrollmentType.DEFAULT, features, mHwPreviewHandle, ctx);
+                    }
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Exception when requesting enroll", e);
+                    onError(BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS,
+                            0 /* vendorCode */);
+                    mCallback.onClientFinished(this, false /* success */);
+                }
+            }, ctx -> {
+                try {
+                    session.getSession().onContextChanged(ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to notify context changed", e);
+                }
+            }, null /* options */);
+        } else {
+            mCancellationSignal = session.getSession().enroll(hat, EnrollmentType.DEFAULT, features,
+                    mHwPreviewHandle);
+        }
+    }
+
+
     @Override
     protected void stopHalOperation() {
         unsubscribeBiometricContext();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 145885d..f7e8123 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -44,6 +44,7 @@
 import android.util.Slog;
 
 import com.android.internal.R;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.CallbackWithProbe;
@@ -297,7 +298,11 @@
         }
 
         try {
-            mCancellationSignal = doAuthenticate();
+            if (Flags.deHidl()) {
+                startAuthentication();
+            } else {
+                mCancellationSignal = doAuthenticate();
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
             onError(
@@ -353,6 +358,51 @@
         return cancel;
     }
 
+    private void startAuthentication() {
+        final AidlSession session = getFreshDaemon();
+        final OperationContextExt opContext = getOperationContext();
+
+        getBiometricContext().subscribe(opContext, ctx -> {
+            try {
+                if (session.hasContextMethods()) {
+                    mCancellationSignal = session.getSession().authenticateWithContext(mOperationId,
+                            ctx);
+                } else {
+                    mCancellationSignal = session.getSession().authenticate(mOperationId);
+                }
+
+                if (getBiometricContext().isAwake()) {
+                    mALSProbeCallback.getProbe().enable();
+                }
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception", e);
+                onError(
+                        BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
+                        0 /* vendorCode */);
+                mSensorOverlays.hide(getSensorId());
+                if (sidefpsControllerRefactor()) {
+                    mAuthenticationStateListeners.onAuthenticationStopped();
+                }
+                mCallback.onClientFinished(this, false /* success */);
+            }
+        }, ctx -> {
+            if (session.hasContextMethods()) {
+                try {
+                    session.getSession().onContextChanged(ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to notify context changed", e);
+                }
+            }
+
+            final boolean isAwake = getBiometricContext().isAwake();
+            if (isAwake) {
+                mALSProbeCallback.getProbe().enable();
+            } else {
+                mALSProbeCallback.getProbe().disable();
+            }
+        }, getOptions());
+    }
+
     @Override
     protected void stopHalOperation() {
         mSensorOverlays.hide(getSensorId());
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index 3aab7b3..a7fb774 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -31,6 +31,7 @@
 import android.util.Slog;
 
 import com.android.server.biometrics.BiometricsProto;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.OperationContextExt;
@@ -105,7 +106,11 @@
                 this);
 
         try {
-            mCancellationSignal = doDetectInteraction();
+            if (Flags.deHidl()) {
+                startDetectInteraction();
+            } else {
+                mCancellationSignal = doDetectInteraction();
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when requesting finger detect", e);
             mSensorOverlays.hide(getSensorId());
@@ -139,6 +144,32 @@
         }
     }
 
+    private void startDetectInteraction() throws RemoteException {
+        final AidlSession session = getFreshDaemon();
+
+        if (session.hasContextMethods()) {
+            final OperationContextExt opContext = getOperationContext();
+            getBiometricContext().subscribe(opContext, ctx -> {
+                try {
+                    mCancellationSignal = session.getSession().detectInteractionWithContext(
+                            ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to start detect interaction", e);
+                    mSensorOverlays.hide(getSensorId());
+                    mCallback.onClientFinished(this, false /* success */);
+                }
+            }, ctx -> {
+                try {
+                    session.getSession().onContextChanged(ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to notify context changed", e);
+                }
+            }, mOptions);
+        } else {
+            mCancellationSignal = session.getSession().detectInteraction();
+        }
+    }
+
     @Override
     public void onInteractionDetected() {
         vibrateSuccess();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index bf5011d..3fb9223 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -39,6 +39,7 @@
 import android.util.Slog;
 import android.view.accessibility.AccessibilityManager;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -200,7 +201,11 @@
 
         BiometricNotificationUtils.cancelBadCalibrationNotification(getContext());
         try {
-            mCancellationSignal = doEnroll();
+            if (Flags.deHidl()) {
+                startEnroll();
+            } else {
+                mCancellationSignal = doEnroll();
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when requesting enroll", e);
             onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS,
@@ -237,6 +242,35 @@
         }
     }
 
+    private void startEnroll() throws RemoteException {
+        final AidlSession session = getFreshDaemon();
+        final HardwareAuthToken hat =
+                HardwareAuthTokenUtils.toHardwareAuthToken(mHardwareAuthToken);
+
+        if (session.hasContextMethods()) {
+            final OperationContextExt opContext = getOperationContext();
+            getBiometricContext().subscribe(opContext, ctx -> {
+                try {
+                    mCancellationSignal = session.getSession().enrollWithContext(
+                            hat, ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Remote exception when requesting enroll", e);
+                    onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS,
+                            0 /* vendorCode */);
+                    mCallback.onClientFinished(this, false /* success */);
+                }
+            }, ctx -> {
+                try {
+                    session.getSession().onContextChanged(ctx);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to notify context changed", e);
+                }
+            }, null /* options */);
+        } else {
+            mCancellationSignal = session.getSession().enroll(hat);
+        }
+    }
+
     @Override
     protected void stopHalOperation() {
         mSensorOverlays.hide(getSensorId());
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index d34661d..34e75c0 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -47,6 +47,7 @@
 import android.media.AudioProfile;
 import android.media.tv.TvInputInfo;
 import android.media.tv.TvInputManager.TvInputCallback;
+import android.os.Handler;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
 
@@ -97,9 +98,15 @@
     private boolean mSystemAudioMute = false;
 
     // If true, do not do routing control/send active source for internal source.
-    // Set to true when the device was woken up by <Text/Image View On>.
+    // Set to true for a short duration when the device is woken up by <Text/Image View On>.
     private boolean mSkipRoutingControl;
 
+    // Handler for posting a runnable to set `mSkipRoutingControl` to false after a delay
+    private final Handler mSkipRoutingControlHandler;
+
+    // Runnable that sets `mSkipRoutingControl` to false
+    private final Runnable mResetSkipRoutingControlRunnable = () -> mSkipRoutingControl = false;
+
     // Message buffer used to buffer selected messages to process later. <Active Source>
     // from a source device, for instance, needs to be buffered if the device is not
     // discovered yet. The buffered commands are taken out and when they are ready to
@@ -162,6 +169,7 @@
                 HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL)
                     == HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED;
         mStandbyHandler = new HdmiCecStandbyModeHandler(service, this);
+        mSkipRoutingControlHandler = new Handler(service.getServiceLooper());
     }
 
     @Override
@@ -184,7 +192,14 @@
         mService.getHdmiCecNetwork().addCecSwitch(
                 mService.getHdmiCecNetwork().getPhysicalAddress());  // TV is a CEC switch too.
         mTvInputs.clear();
+
         mSkipRoutingControl = (reason == HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE);
+        mSkipRoutingControlHandler.removeCallbacks(mResetSkipRoutingControlRunnable);
+        if (mSkipRoutingControl) {
+            mSkipRoutingControlHandler.postDelayed(mResetSkipRoutingControlRunnable,
+                    HdmiConfig.TIMEOUT_MS);
+        }
+
         launchRoutingControl(reason != HdmiControlService.INITIATED_BY_ENABLE_CEC &&
                 reason != HdmiControlService.INITIATED_BY_BOOT_UP);
         resetSelectRequestBuffer();
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index fb4943a..67c23fc 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1328,8 +1328,7 @@
             mPointerIconDisplayContext = null;
         }
 
-        updateAdditionalDisplayInputProperties(displayId,
-                AdditionalDisplayInputProperties::reset);
+        updateAdditionalDisplayInputProperties(displayId, AdditionalDisplayInputProperties::reset);
 
         // TODO(b/320763728): Rely on WindowInfosListener to determine when a display has been
         //  removed in InputDispatcher instead of this callback.
@@ -1812,8 +1811,6 @@
             mPointerIconType = icon.getType();
             mPointerIcon = mPointerIconType == PointerIcon.TYPE_CUSTOM ? icon : null;
 
-            if (!mCurrentDisplayProperties.pointerIconVisible) return false;
-
             return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
         }
     }
@@ -3478,6 +3475,10 @@
     private void applyAdditionalDisplayInputPropertiesLocked(
             AdditionalDisplayInputProperties properties) {
         // Handle changes to each of the individual properties.
+        // TODO(b/293587049): This approach for updating pointer display properties is only for when
+        //  PointerChoreographer is disabled. Remove this logic when PointerChoreographer is
+        //  permanently enabled.
+
         if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) {
             mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible;
             if (properties.pointerIconVisible) {
@@ -3496,7 +3497,6 @@
                 != mCurrentDisplayProperties.mousePointerAccelerationEnabled) {
             mCurrentDisplayProperties.mousePointerAccelerationEnabled =
                     properties.mousePointerAccelerationEnabled;
-            mNative.setMousePointerAccelerationEnabled(properties.mousePointerAccelerationEnabled);
         }
     }
 
@@ -3509,7 +3509,16 @@
                 properties = new AdditionalDisplayInputProperties();
                 mAdditionalDisplayInputProperties.put(displayId, properties);
             }
+            final boolean oldPointerIconVisible = properties.pointerIconVisible;
+            final boolean oldMouseAccelerationEnabled = properties.mousePointerAccelerationEnabled;
             updater.accept(properties);
+            if (oldPointerIconVisible != properties.pointerIconVisible) {
+                mNative.setPointerIconVisibility(displayId, properties.pointerIconVisible);
+            }
+            if (oldMouseAccelerationEnabled != properties.mousePointerAccelerationEnabled) {
+                mNative.setMousePointerAccelerationEnabled(displayId,
+                        properties.mousePointerAccelerationEnabled);
+            }
             if (properties.allDefaults()) {
                 mAdditionalDisplayInputProperties.remove(displayId);
             }
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index c3ef80f..6f52020 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -119,7 +119,7 @@
 
     void setPointerSpeed(int speed);
 
-    void setMousePointerAccelerationEnabled(boolean enabled);
+    void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
 
     void setTouchpadPointerSpeed(int speed);
 
@@ -190,6 +190,8 @@
     boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, int pointerId,
             @NonNull IBinder inputToken);
 
+    void setPointerIconVisibility(int displayId, boolean visible);
+
     void requestPointerCapture(IBinder windowToken, boolean enabled);
 
     boolean canDispatchToDisplay(int deviceId, int displayId);
@@ -352,7 +354,7 @@
         public native void setPointerSpeed(int speed);
 
         @Override
-        public native void setMousePointerAccelerationEnabled(boolean enabled);
+        public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
 
         @Override
         public native void setTouchpadPointerSpeed(int speed);
@@ -452,6 +454,9 @@
                 int pointerId, IBinder inputToken);
 
         @Override
+        public native void setPointerIconVisibility(int displayId, boolean visible);
+
+        @Override
         public native void requestPointerCapture(IBinder windowToken, boolean enabled);
 
         @Override
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
index c76ca2b..fba71fd 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
@@ -114,7 +114,7 @@
      * @param userId      The user ID to be associated with.
      */
     static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
-            ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+            InputMethodMap methodMap, @UserIdInt int userId) {
         final File inputMethodDir = getInputMethodDir(userId);
 
         if (allSubtypes.isEmpty()) {
@@ -143,7 +143,7 @@
 
     @VisibleForTesting
     static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
-            ArrayMap<String, InputMethodInfo> methodMap, AtomicFile subtypesFile) {
+            InputMethodMap methodMap, AtomicFile subtypesFile) {
         // Safety net for the case that this function is called before methodMap is set.
         final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0;
         FileOutputStream fos = null;
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index 4b85d09..62adb25 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -20,7 +20,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.util.ArrayMap;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
@@ -44,24 +43,23 @@
         return mUserId;
     }
 
-    HardwareKeyboardShortcutController(
-            @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+    HardwareKeyboardShortcutController(@NonNull InputMethodMap methodMap, @UserIdInt int userId) {
         mUserId = userId;
         reset(methodMap);
     }
 
     @GuardedBy("ImfLock.class")
-    void reset(@NonNull ArrayMap<String, InputMethodInfo> methodMap) {
+    void reset(@NonNull InputMethodMap methodMap) {
         mSubtypeHandles.clear();
-        final InputMethodSettings settings = new InputMethodSettings(methodMap, mUserId);
-        final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodListLocked();
+        final InputMethodSettings settings = InputMethodSettings.create(methodMap, mUserId);
+        final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodList();
         for (int i = 0; i < inputMethods.size(); ++i) {
             final InputMethodInfo imi = inputMethods.get(i);
             if (!imi.shouldShowInInputMethodPicker()) {
                 continue;
             }
             final List<InputMethodSubtype> subtypes =
-                    settings.getEnabledInputMethodSubtypeListLocked(imi, true);
+                    settings.getEnabledInputMethodSubtypeList(imi, true);
             if (subtypes.isEmpty()) {
                 mSubtypeHandles.add(InputMethodSubtypeHandle.of(imi, null));
             } else {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
index 542165d..6339686 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
@@ -23,7 +23,6 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.text.TextUtils;
-import android.util.ArrayMap;
 import android.util.Slog;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
@@ -204,7 +203,7 @@
      */
     @Nullable
     static InputMethodInfo chooseSystemVoiceIme(
-            @NonNull ArrayMap<String, InputMethodInfo> methodMap,
+            @NonNull InputMethodMap methodMap,
             @Nullable String systemSpeechRecognizerPackageName,
             @Nullable String currentDefaultVoiceImeId) {
         if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 8448fc2..beb68d3 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -314,10 +314,6 @@
     @Nullable
     private VirtualDeviceManagerInternal mVdmInternal = null;
 
-    // All known input methods.
-    final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>();
-    private final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>();
-
     // Mapping from deviceId to the device-specific imeId for that device.
     @GuardedBy("ImfLock.class")
     private final SparseArray<String> mVirtualDeviceMethodMap = new SparseArray<>();
@@ -330,7 +326,7 @@
     private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
 
     /**
-     * Tracks how many times {@link #mMethodMap} was updated.
+     * Tracks how many times {@link #mSettings} was updated.
      */
     @GuardedBy("ImfLock.class")
     private int mMethodMapUpdateCount = 0;
@@ -472,7 +468,7 @@
     @GuardedBy("ImfLock.class")
     @Nullable
     InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) {
-        return mMethodMap.get(imeId);
+        return mSettings.getMethodMap().get(imeId);
     }
 
     /**
@@ -1265,10 +1261,11 @@
                     return false;
                 }
                 String curInputMethodId = mSettings.getSelectedInputMethod();
-                final int numImes = mMethodList.size();
+                final List<InputMethodInfo> methodList = mSettings.getMethodList();
+                final int numImes = methodList.size();
                 if (curInputMethodId != null) {
                     for (int i = 0; i < numImes; i++) {
-                        InputMethodInfo imi = mMethodList.get(i);
+                        InputMethodInfo imi = methodList.get(i);
                         if (imi.getId().equals(curInputMethodId)) {
                             for (String pkg : packages) {
                                 if (imi.getPackageName().equals(pkg)) {
@@ -1339,7 +1336,7 @@
         @Override
         public void onPackageDataCleared(String packageName, int uid) {
             boolean changed = false;
-            for (InputMethodInfo imi : mMethodList) {
+            for (InputMethodInfo imi : mSettings.getMethodList()) {
                 if (imi.getPackageName().equals(packageName)) {
                     mAdditionalSubtypeMap.remove(imi.getId());
                     changed = true;
@@ -1347,7 +1344,8 @@
             }
             if (changed) {
                 AdditionalSubtypeUtils.save(
-                        mAdditionalSubtypeMap, mMethodMap, mSettings.getCurrentUserId());
+                        mAdditionalSubtypeMap, mSettings.getMethodMap(),
+                        mSettings.getCurrentUserId());
                 mChangedPackages.add(packageName);
             }
         }
@@ -1405,10 +1403,11 @@
 
                 InputMethodInfo curIm = null;
                 String curInputMethodId = mSettings.getSelectedInputMethod();
-                final int numImes = mMethodList.size();
+                final List<InputMethodInfo> methodList = mSettings.getMethodList();
+                final int numImes = methodList.size();
                 if (curInputMethodId != null) {
                     for (int i = 0; i < numImes; i++) {
-                        InputMethodInfo imi = mMethodList.get(i);
+                        InputMethodInfo imi = methodList.get(i);
                         final String imiId = imi.getId();
                         if (imiId.equals(curInputMethodId)) {
                             curIm = imi;
@@ -1426,7 +1425,7 @@
                                             + imi.getComponent());
                             mAdditionalSubtypeMap.remove(imi.getId());
                             AdditionalSubtypeUtils.save(mAdditionalSubtypeMap,
-                                    mMethodMap,
+                                    mSettings.getMethodMap(),
                                     mSettings.getCurrentUserId());
                         }
                     }
@@ -1584,7 +1583,7 @@
             if (userId != currentUserId) {
                 return;
             }
-            mSettings = new InputMethodSettings(mMethodMap, userId);
+            mSettings = InputMethodSettings.createEmptyMap(userId);
             if (mSystemReady) {
                 // We need to rebuild IMEs.
                 buildInputMethodListLocked(false /* resetDefaultEnabledIme */);
@@ -1658,14 +1657,15 @@
         mLastSwitchUserId = userId;
 
         // mSettings should be created before buildInputMethodListLocked
-        mSettings = new InputMethodSettings(mMethodMap, userId);
+        mSettings = InputMethodSettings.createEmptyMap(userId);
 
         AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
         mSwitchingController =
-                InputMethodSubtypeSwitchingController.createInstanceLocked(context, mMethodMap,
-                        userId);
+                InputMethodSubtypeSwitchingController.createInstanceLocked(context,
+                        mSettings.getMethodMap(), userId);
         mHardwareKeyboardShortcutController =
-                new HardwareKeyboardShortcutController(mMethodMap, userId);
+                new HardwareKeyboardShortcutController(mSettings.getMethodMap(),
+                        mSettings.getCurrentUserId());
         mMenuController = new InputMethodMenuController(this);
         mBindingController =
                 bindingControllerForTesting != null
@@ -1723,11 +1723,12 @@
     private void resetDefaultImeLocked(Context context) {
         // Do not reset the default (current) IME when it is a 3rd-party IME
         String selectedMethodId = getSelectedMethodIdLocked();
-        if (selectedMethodId != null && !mMethodMap.get(selectedMethodId).isSystem()) {
+        if (selectedMethodId != null
+                && !mSettings.getMethodMap().get(selectedMethodId).isSystem()) {
             return;
         }
         final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes(
-                context, mSettings.getEnabledInputMethodListLocked());
+                context, mSettings.getEnabledInputMethodList());
         if (suitableImes.isEmpty()) {
             Slog.i(TAG, "No default found");
             return;
@@ -1791,7 +1792,7 @@
         // ContentObserver should be registered again when the user is changed
         mSettingsObserver.registerContentObserverLocked(newUserId);
 
-        mSettings = new InputMethodSettings(mMethodMap, newUserId);
+        mSettings = InputMethodSettings.createEmptyMap(newUserId);
         // Additional subtypes should be reset when the user is changed
         AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId);
         final String defaultImiId = mSettings.getSelectedInputMethod();
@@ -1822,7 +1823,7 @@
         if (initialUserSwitch) {
             InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
                     getPackageManagerForUser(mContext, newUserId),
-                    mSettings.getEnabledInputMethodListLocked());
+                    mSettings.getEnabledInputMethodList());
         }
 
         if (DEBUG) {
@@ -1899,7 +1900,7 @@
                 updateFromSettingsLocked(true);
                 InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
                         getPackageManagerForUser(mContext, currentUserId),
-                        mSettings.getEnabledInputMethodListLocked());
+                        mSettings.getEnabledInputMethodList());
             }
         }
     }
@@ -2001,9 +2002,9 @@
             }
             //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList.
             //TODO(b/210039666): use cache.
-            final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-            final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
-            final InputMethodInfo imi = methodMap.get(settings.getSelectedInputMethod());
+            final InputMethodSettings settings = queryMethodMapForUser(userId);
+            final InputMethodInfo imi = settings.getMethodMap().get(
+                    settings.getSelectedInputMethod());
             return imi != null && imi.supportsStylusHandwriting();
         }
     }
@@ -2023,23 +2024,19 @@
     @GuardedBy("ImfLock.class")
     private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId,
             @DirectBootAwareness int directBootAwareness, int callingUid) {
-        final ArrayList<InputMethodInfo> methodList;
         final InputMethodSettings settings;
         if (userId == mSettings.getCurrentUserId()
                 && directBootAwareness == DirectBootAwareness.AUTO) {
-            // Create a copy.
-            methodList = new ArrayList<>(mMethodList);
             settings = mSettings;
         } else {
-            final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
-            methodList = new ArrayList<>();
             final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
                     new ArrayMap<>();
             AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
-            queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
-                    methodList, directBootAwareness);
-            settings = new InputMethodSettings(methodMap, userId);
+            settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
+                    directBootAwareness);
         }
+        // Create a copy.
+        final ArrayList<InputMethodInfo> methodList = new ArrayList<>(settings.getMethodList());
         // filter caller's access to input methods
         methodList.removeIf(imi ->
                 !canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings));
@@ -2052,12 +2049,11 @@
         final ArrayList<InputMethodInfo> methodList;
         final InputMethodSettings settings;
         if (userId == mSettings.getCurrentUserId()) {
-            methodList = mSettings.getEnabledInputMethodListLocked();
+            methodList = mSettings.getEnabledInputMethodList();
             settings = mSettings;
         } else {
-            final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-            settings = new InputMethodSettings(methodMap, userId);
-            methodList = settings.getEnabledInputMethodListLocked();
+            settings = queryMethodMapForUser(userId);
+            methodList = settings.getEnabledInputMethodList();
         }
         // filter caller's access to input methods
         methodList.removeIf(imi ->
@@ -2120,27 +2116,26 @@
             final InputMethodInfo imi;
             String selectedMethodId = getSelectedMethodIdLocked();
             if (imiId == null && selectedMethodId != null) {
-                imi = mMethodMap.get(selectedMethodId);
+                imi = mSettings.getMethodMap().get(selectedMethodId);
             } else {
-                imi = mMethodMap.get(imiId);
+                imi = mSettings.getMethodMap().get(imiId);
             }
             if (imi == null || !canCallerAccessInputMethod(
                     imi.getPackageName(), callingUid, userId, mSettings)) {
                 return Collections.emptyList();
             }
-            return mSettings.getEnabledInputMethodSubtypeListLocked(
+            return mSettings.getEnabledInputMethodSubtypeList(
                     imi, allowsImplicitlyEnabledSubtypes);
         }
-        final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-        final InputMethodInfo imi = methodMap.get(imiId);
+        final InputMethodSettings settings = queryMethodMapForUser(userId);
+        final InputMethodInfo imi = settings.getMethodMap().get(imiId);
         if (imi == null) {
             return Collections.emptyList();
         }
-        final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
         if (!canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)) {
             return Collections.emptyList();
         }
-        return settings.getEnabledInputMethodSubtypeListLocked(
+        return settings.getEnabledInputMethodSubtypeList(
                 imi, allowsImplicitlyEnabledSubtypes);
     }
 
@@ -2343,7 +2338,7 @@
         }
 
         String curId = getCurIdLocked();
-        final InputMethodInfo curInputMethodInfo = mMethodMap.get(curId);
+        final InputMethodInfo curInputMethodInfo = mSettings.getMethodMap().get(curId);
         final boolean suppressesSpellChecker =
                 curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
         final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
@@ -2559,7 +2554,7 @@
                 mVirtualDeviceMethodMap.get(mDeviceIdToShowIme, currentMethodId);
         if (Objects.equals(deviceMethodId, currentMethodId)) {
             return currentMethodId;
-        } else if (!mMethodMap.containsKey(deviceMethodId)) {
+        } else if (!mSettings.getMethodMap().containsKey(deviceMethodId)) {
             if (DEBUG) {
                 Slog.v(TAG, "Disabling IME on virtual device with id " + mDeviceIdToShowIme
                         + " because its custom input method is not available: " + deviceMethodId);
@@ -2601,7 +2596,7 @@
         if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) {
             return false;
         }
-        final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
+        final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId);
         if (imi == null) {
             return false;
         }
@@ -3023,7 +3018,7 @@
             return false;
         }
 
-        List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilterLocked(
+        List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilter(
                 InputMethodInfo::shouldShowInInputMethodPicker);
         final int numImes = imes.size();
         if (numImes > 2) return true;
@@ -3035,7 +3030,7 @@
         for (int i = 0; i < numImes; ++i) {
             final InputMethodInfo imi = imes.get(i);
             final List<InputMethodSubtype> subtypes =
-                    mSettings.getEnabledInputMethodSubtypeListLocked(imi, true);
+                    mSettings.getEnabledInputMethodSubtypeList(imi, true);
             final int subtypeCount = subtypes.size();
             if (subtypeCount == 0) {
                 ++nonAuxCount;
@@ -3187,7 +3182,7 @@
             final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext,
                     mSettings.getCurrentUserId());
 
-            List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked();
+            List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList();
             for (int i = 0; i < enabled.size(); i++) {
                 // We allow the user to select "disabled until used" apps, so if they
                 // are enabling one of those here we now need to make it enabled.
@@ -3234,17 +3229,17 @@
 
         // TODO: Instantiate mSwitchingController for each user.
         if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) {
-            mSwitchingController.resetCircularListLocked(mMethodMap);
+            mSwitchingController.resetCircularListLocked(mSettings.getMethodMap());
         } else {
             mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
-                    mContext, mMethodMap, mSettings.getCurrentUserId());
+                    mContext, mSettings.getMethodMap(), mSettings.getCurrentUserId());
         }
         // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
         if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) {
-            mHardwareKeyboardShortcutController.reset(mMethodMap);
+            mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap());
         } else {
             mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
-                    mMethodMap, mSettings.getCurrentUserId());
+                    mSettings.getMethodMap(), mSettings.getCurrentUserId());
         }
         sendOnNavButtonFlagsChangedLocked();
     }
@@ -3268,7 +3263,7 @@
 
     @GuardedBy("ImfLock.class")
     void setInputMethodLocked(String id, int subtypeId, int deviceId) {
-        InputMethodInfo info = mMethodMap.get(id);
+        InputMethodInfo info = mSettings.getMethodMap().get(id);
         if (info == null) {
             throw getExceptionForUnknownImeId(id);
         }
@@ -4017,7 +4012,7 @@
             if (!calledWithValidTokenLocked(token)) {
                 return;
             }
-            final InputMethodInfo imi = mMethodMap.get(id);
+            final InputMethodInfo imi = mSettings.getMethodMap().get(id);
             if (imi == null || !canCallerAccessInputMethod(
                     imi.getPackageName(), callingUid, userId, mSettings)) {
                 throw getExceptionForUnknownImeId(id);
@@ -4035,7 +4030,7 @@
             if (!calledWithValidTokenLocked(token)) {
                 return;
             }
-            final InputMethodInfo imi = mMethodMap.get(id);
+            final InputMethodInfo imi = mSettings.getMethodMap().get(id);
             if (imi == null || !canCallerAccessInputMethod(
                     imi.getPackageName(), callingUid, userId, mSettings)) {
                 throw getExceptionForUnknownImeId(id);
@@ -4055,10 +4050,10 @@
             if (!calledWithValidTokenLocked(token)) {
                 return false;
             }
-            final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked();
+            final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtype();
             final InputMethodInfo lastImi;
             if (lastIme != null) {
-                lastImi = mMethodMap.get(lastIme.first);
+                lastImi = mSettings.getMethodMap().get(lastIme.first);
             } else {
                 lastImi = null;
             }
@@ -4082,7 +4077,7 @@
                 // This is a safety net. If the currentSubtype can't be added to the history
                 // and the framework couldn't find the last ime, we will make the last ime be
                 // the most applicable enabled keyboard subtype of the system imes.
-                final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked();
+                final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList();
                 if (enabled != null) {
                     final int enabledCount = enabled.size();
                     final String locale;
@@ -4097,7 +4092,7 @@
                         final InputMethodInfo imi = enabled.get(i);
                         if (imi.getSubtypeCount() > 0 && imi.isSystem()) {
                             InputMethodSubtype keyboardSubtype =
-                                    SubtypeUtils.findLastResortApplicableSubtypeLocked(
+                                    SubtypeUtils.findLastResortApplicableSubtype(
                                             SubtypeUtils.getSubtypes(imi),
                                             SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true);
                             if (keyboardSubtype != null) {
@@ -4139,7 +4134,8 @@
     @GuardedBy("ImfLock.class")
     private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) {
         final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
-                onlyCurrentIme, mMethodMap.get(getSelectedMethodIdLocked()), mCurrentSubtype);
+                onlyCurrentIme, mSettings.getMethodMap().get(getSelectedMethodIdLocked()),
+                mCurrentSubtype);
         if (nextSubtype == null) {
             return false;
         }
@@ -4155,8 +4151,8 @@
                 return false;
             }
             final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
-                    false /* onlyCurrentIme */, mMethodMap.get(getSelectedMethodIdLocked()),
-                    mCurrentSubtype);
+                    false /* onlyCurrentIme */,
+                    mSettings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype);
             return nextSubtype != null;
         }
     }
@@ -4169,12 +4165,11 @@
         }
         synchronized (ImfLock.class) {
             if (mSettings.getCurrentUserId() == userId) {
-                return mSettings.getLastInputMethodSubtypeLocked();
+                return mSettings.getLastInputMethodSubtype();
             }
 
-            final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-            final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
-            return settings.getLastInputMethodSubtypeLocked();
+            final InputMethodSettings settings = queryMethodMapForUser(userId);
+            return settings.getLastInputMethodSubtype();
         }
     }
 
@@ -4218,14 +4213,11 @@
                 return;
             }
 
-            final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
-            final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
             final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
                     new ArrayMap<>();
             AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
-            queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
-                    methodList, DirectBootAwareness.AUTO);
-            final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
+            final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId,
+                    additionalSubtypeMap, DirectBootAwareness.AUTO);
             settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap,
                     mPackageManagerInternal, callingUid);
         }
@@ -4253,8 +4245,7 @@
             synchronized (ImfLock.class) {
                 final boolean currentUser = (mSettings.getCurrentUserId() == userId);
                 final InputMethodSettings settings = currentUser
-                        ? mSettings
-                        : new InputMethodSettings(queryMethodMapForUser(userId), userId);
+                        ? mSettings : queryMethodMapForUser(userId);
                 if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) {
                     return;
                 }
@@ -4626,7 +4617,8 @@
             if (mSettings.getCurrentUserId() != mSwitchingController.getUserId()) {
                 return;
             }
-            final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked());
+            final InputMethodInfo imi =
+                    mSettings.getMethodMap().get(getSelectedMethodIdLocked());
             if (imi != null) {
                 mSwitchingController.onUserActionLocked(imi, mCurrentSubtype);
             }
@@ -4684,8 +4676,8 @@
             return;
         } else {
             // Called with current IME's token.
-            if (mMethodMap.get(id) != null
-                    && mSettings.getEnabledInputMethodListWithFilterLocked(
+            if (mSettings.getMethodMap().get(id) != null
+                    && mSettings.getEnabledInputMethodListWithFilter(
                             (info) -> info.getId().equals(id)).isEmpty()) {
                 throw new IllegalStateException("Requested IME is not enabled: " + id);
             }
@@ -4866,7 +4858,8 @@
                     final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
                             .getSortedInputMethodAndSubtypeList(
                                     showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
-                                    mContext, mMethodMap, mSettings.getCurrentUserId());
+                                    mContext, mSettings.getMethodMap(),
+                                    mSettings.getCurrentUserId());
                     mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
                             lastInputMethodId, lastInputMethodSubtypeId, imList);
                 }
@@ -5050,7 +5043,7 @@
     @GuardedBy("ImfLock.class")
     private boolean chooseNewDefaultIMELocked() {
         final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME(
-                mSettings.getEnabledInputMethodListLocked());
+                mSettings.getEnabledInputMethodList());
         if (imi != null) {
             if (DEBUG) {
                 Slog.d(TAG, "New default IME was selected: " + imi.getId());
@@ -5062,17 +5055,14 @@
         return false;
     }
 
-    static void queryInputMethodServicesInternal(Context context,
+    @NonNull
+    static InputMethodSettings queryInputMethodServicesInternal(Context context,
             @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
-            ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
             @DirectBootAwareness int directBootAwareness) {
         final Context userAwareContext = context.getUserId() == userId
                 ? context
                 : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
 
-        methodList.clear();
-        methodMap.clear();
-
         final int directBootAwarenessFlags;
         switch (directBootAwareness) {
             case DirectBootAwareness.ANY:
@@ -5095,24 +5085,23 @@
                 new Intent(InputMethod.SERVICE_INTERFACE),
                 PackageManager.ResolveInfoFlags.of(flags));
 
-        methodList.ensureCapacity(services.size());
-        methodMap.ensureCapacity(services.size());
-
         // Note: This is a temporary solution for Bug 261723412.  If there is any better solution,
         // we should remove this data dependency.
         final List<String> enabledInputMethodList =
                 InputMethodUtils.getEnabledInputMethodIdsForFiltering(context, userId);
 
-        filterInputMethodServices(additionalSubtypeMap, methodMap, methodList,
-                enabledInputMethodList, userAwareContext, services);
+        final InputMethodMap methodMap = filterInputMethodServices(
+                additionalSubtypeMap, enabledInputMethodList, userAwareContext, services);
+        return InputMethodSettings.create(methodMap, userId);
     }
 
-    static void filterInputMethodServices(
+    @NonNull
+    static InputMethodMap filterInputMethodServices(
             ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
-            ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
             List<String> enabledInputMethodList, Context userAwareContext,
             List<ResolveInfo> services) {
         final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>();
+        final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(services.size());
 
         for (int i = 0; i < services.size(); ++i) {
             ResolveInfo ri = services.get(i);
@@ -5141,7 +5130,6 @@
                     imiPackageCount.put(packageName,
                             1 + imiPackageCount.getOrDefault(packageName, 0));
 
-                    methodList.add(imi);
                     methodMap.put(imi.getId(), imi);
                     if (DEBUG) {
                         Slog.d(TAG, "Found an input method " + imi);
@@ -5153,6 +5141,7 @@
                 Slog.wtf(TAG, "Unable to load input method " + imeId, e);
             }
         }
+        return InputMethodMap.of(methodMap);
     }
 
     @GuardedBy("ImfLock.class")
@@ -5168,8 +5157,8 @@
         mMethodMapUpdateCount++;
         mMyPackageMonitor.clearKnownImePackageNamesLocked();
 
-        queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(),
-                mAdditionalSubtypeMap, mMethodMap, mMethodList, DirectBootAwareness.AUTO);
+        mSettings = queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(),
+                mAdditionalSubtypeMap, DirectBootAwareness.AUTO);
 
         // Construct the set of possible IME packages for onPackageChanged() to avoid false
         // negatives when the package state remains to be the same but only the component state is
@@ -5196,11 +5185,11 @@
         if (!resetDefaultEnabledIme) {
             boolean enabledImeFound = false;
             boolean enabledNonAuxImeFound = false;
-            final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodListLocked();
+            final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodList();
             final int numImes = enabledImes.size();
             for (int i = 0; i < numImes; ++i) {
                 final InputMethodInfo imi = enabledImes.get(i);
-                if (mMethodList.contains(imi)) {
+                if (mSettings.getMethodMap().containsKey(imi.getId())) {
                     enabledImeFound = true;
                     if (!imi.isAuxiliaryIme()) {
                         enabledNonAuxImeFound = true;
@@ -5224,7 +5213,7 @@
 
         if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) {
             final ArrayList<InputMethodInfo> defaultEnabledIme =
-                    InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList,
+                    InputMethodInfoUtils.getDefaultEnabledImes(mContext, mSettings.getMethodList(),
                             reenableMinimumNonAuxSystemImes);
             final int numImes = defaultEnabledIme.size();
             for (int i = 0; i < numImes; ++i) {
@@ -5238,7 +5227,7 @@
 
         final String defaultImiId = mSettings.getSelectedInputMethod();
         if (!TextUtils.isEmpty(defaultImiId)) {
-            if (!mMethodMap.containsKey(defaultImiId)) {
+            if (!mSettings.getMethodMap().containsKey(defaultImiId)) {
                 Slog.w(TAG, "Default IME is uninstalled. Choose new default IME.");
                 if (chooseNewDefaultIMELocked()) {
                     updateInputMethodsFromSettingsLocked(true);
@@ -5253,23 +5242,23 @@
 
         // TODO: Instantiate mSwitchingController for each user.
         if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) {
-            mSwitchingController.resetCircularListLocked(mMethodMap);
+            mSwitchingController.resetCircularListLocked(mSettings.getMethodMap());
         } else {
             mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
-                    mContext, mMethodMap, mSettings.getCurrentUserId());
+                    mContext, mSettings.getMethodMap(), mSettings.getCurrentUserId());
         }
         // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
         if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) {
-            mHardwareKeyboardShortcutController.reset(mMethodMap);
+            mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap());
         } else {
             mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
-                    mMethodMap, mSettings.getCurrentUserId());
+                    mSettings.getMethodMap(), mSettings.getCurrentUserId());
         }
 
         sendOnNavButtonFlagsChangedLocked();
 
         // Notify InputMethodListListeners of the new installed InputMethods.
-        final List<InputMethodInfo> inputMethodList = new ArrayList<>(mMethodList);
+        final List<InputMethodInfo> inputMethodList = mSettings.getMethodList();
         mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED,
                 mSettings.getCurrentUserId(), 0 /* unused */, inputMethodList).sendToTarget();
     }
@@ -5290,7 +5279,7 @@
                 mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer);
         final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod();
         final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme(
-                mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId);
+                mSettings.getMethodMap(), systemSpeechRecognizer, currentDefaultVoiceImeId);
         if (newSystemVoiceIme == null) {
             if (DEBUG) {
                 Slog.i(TAG, "Found no valid default Voice IME. If the user is still locked,"
@@ -5341,9 +5330,9 @@
             return false;
         } else {
             final List<Pair<String, ArrayList<String>>> enabledInputMethodsList = mSettings
-                    .getEnabledInputMethodsAndSubtypeListLocked();
+                    .getEnabledInputMethodsAndSubtypeList();
             StringBuilder builder = new StringBuilder();
-            if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+            if (mSettings.buildAndPutEnabledInputMethodsStrRemovingId(
                     builder, enabledInputMethodsList, id)) {
                 if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
                     // Disabled input method is currently selected, switch to another one.
@@ -5357,7 +5346,7 @@
                     // new default one but only update the settings.
                     InputMethodInfo newDefaultIme =
                             InputMethodInfoUtils.getMostApplicableDefaultIME(
-                                        mSettings.getEnabledInputMethodListLocked());
+                                        mSettings.getEnabledInputMethodList());
                     mSettings.putSelectedDefaultDeviceInputMethod(
                             newDefaultIme == null ? "" : newDefaultIme.getId());
                 }
@@ -5402,11 +5391,11 @@
 
     @GuardedBy("ImfLock.class")
     private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) {
-        InputMethodInfo imi = mMethodMap.get(newDefaultIme);
+        InputMethodInfo imi = mSettings.getMethodMap().get(newDefaultIme);
         int lastSubtypeId = NOT_A_SUBTYPE_ID;
         // newDefaultIme is empty when there is no candidate for the selected IME.
         if (imi != null && !TextUtils.isEmpty(newDefaultIme)) {
-            String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme);
+            String subtypeHashCode = mSettings.getLastSubtypeForInputMethod(newDefaultIme);
             if (subtypeHashCode != null) {
                 try {
                     lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi,
@@ -5437,8 +5426,7 @@
                 return getCurrentInputMethodSubtypeLocked();
             }
 
-            final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-            final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
+            final InputMethodSettings settings = queryMethodMapForUser(userId);
             return settings.getCurrentInputMethodSubtypeForNonCurrentUsers();
         }
     }
@@ -5460,7 +5448,7 @@
             return null;
         }
         final boolean subtypeIsSelected = mSettings.isSubtypeSelected();
-        final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
+        final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId);
         if (imi == null || imi.getSubtypeCount() == 0) {
             return null;
         }
@@ -5472,7 +5460,7 @@
                 // the most applicable subtype from explicitly or implicitly enabled
                 // subtypes.
                 List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
-                        mSettings.getEnabledInputMethodSubtypeListLocked(imi, true);
+                        mSettings.getEnabledInputMethodSubtypeList(imi, true);
                 // If there is only one explicitly or implicitly enabled subtype,
                 // just returns it.
                 if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
@@ -5480,11 +5468,11 @@
                 } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) {
                     final String locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId())
                             .get(0).toString();
-                    mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+                    mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype(
                             explicitlyOrImplicitlyEnabledSubtypes,
                             SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true);
                     if (mCurrentSubtype == null) {
-                        mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+                        mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype(
                                 explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
                     }
                 }
@@ -5501,46 +5489,42 @@
      */
     @GuardedBy("ImfLock.class")
     private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) {
+        final InputMethodSettings settings;
         if (userId == mSettings.getCurrentUserId()) {
-            return mMethodMap.get(mSettings.getSelectedInputMethod());
+            settings = mSettings;
+        } else {
+            final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
+                    new ArrayMap<>();
+            AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+            settings = queryInputMethodServicesInternal(mContext, userId,
+                    additionalSubtypeMap, DirectBootAwareness.AUTO);
         }
-
-        final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
-        final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
-        final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>();
-        AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
-        queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
-                methodList, DirectBootAwareness.AUTO);
-        InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
-        return methodMap.get(settings.getSelectedInputMethod());
+        return settings.getMethodMap().get(settings.getSelectedInputMethod());
     }
 
-    private ArrayMap<String, InputMethodInfo> queryMethodMapForUser(@UserIdInt int userId) {
-        final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
-        final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
+    private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) {
         final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
                 new ArrayMap<>();
         AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
-        queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
-                methodMap, methodList, DirectBootAwareness.AUTO);
-        return methodMap;
+        return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
+                DirectBootAwareness.AUTO);
     }
 
     @GuardedBy("ImfLock.class")
     private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) {
         if (userId == mSettings.getCurrentUserId()) {
-            if (!mMethodMap.containsKey(imeId)
-                    || !mSettings.getEnabledInputMethodListLocked()
-                    .contains(mMethodMap.get(imeId))) {
+            if (!mSettings.getMethodMap().containsKey(imeId)
+                    || !mSettings.getEnabledInputMethodList()
+                    .contains(mSettings.getMethodMap().get(imeId))) {
                 return false; // IME is not found or not enabled.
             }
             setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID);
             return true;
         }
-        final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-        final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
-        if (!methodMap.containsKey(imeId)
-                || !settings.getEnabledInputMethodListLocked().contains(methodMap.get(imeId))) {
+        final InputMethodSettings settings = queryMethodMapForUser(userId);
+        if (!settings.getMethodMap().containsKey(imeId)
+                || !settings.getEnabledInputMethodList().contains(
+                        settings.getMethodMap().get(imeId))) {
             return false; // IME is not found or not enabled.
         }
         settings.putSelectedInputMethod(imeId);
@@ -5578,7 +5562,8 @@
 
     @GuardedBy("ImfLock.class")
     private void switchKeyboardLayoutLocked(int direction) {
-        final InputMethodInfo currentImi = mMethodMap.get(getSelectedMethodIdLocked());
+        final InputMethodInfo currentImi = mSettings.getMethodMap().get(
+                getSelectedMethodIdLocked());
         if (currentImi == null) {
             return;
         }
@@ -5590,7 +5575,7 @@
         if (nextSubtypeHandle == null) {
             return;
         }
-        final InputMethodInfo nextImi = mMethodMap.get(nextSubtypeHandle.getImeId());
+        final InputMethodInfo nextImi = mSettings.getMethodMap().get(nextSubtypeHandle.getImeId());
         if (nextImi == null) {
             return;
         }
@@ -5670,15 +5655,14 @@
         public boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) {
             synchronized (ImfLock.class) {
                 if (userId == mSettings.getCurrentUserId()) {
-                    if (!mMethodMap.containsKey(imeId)) {
+                    if (!mSettings.getMethodMap().containsKey(imeId)) {
                         return false; // IME is not found.
                     }
                     setInputMethodEnabledLocked(imeId, enabled);
                     return true;
                 }
-                final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-                final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
-                if (!methodMap.containsKey(imeId)) {
+                final InputMethodSettings settings = queryMethodMapForUser(userId);
+                if (!settings.getMethodMap().containsKey(imeId)) {
                     return false; // IME is not found.
                 }
                 if (enabled) {
@@ -5689,9 +5673,9 @@
                         settings.putEnabledInputMethodsStr(newEnabledImeIdsStr);
                     }
                 } else {
-                    settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+                    settings.buildAndPutEnabledInputMethodsStrRemovingId(
                             new StringBuilder(),
-                            settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId);
+                            settings.getEnabledInputMethodsAndSubtypeList(), imeId);
                 }
                 return true;
             }
@@ -5997,10 +5981,11 @@
 
         synchronized (ImfLock.class) {
             p.println("Current Input Method Manager state:");
-            int numImes = mMethodList.size();
+            final List<InputMethodInfo> methodList = mSettings.getMethodList();
+            int numImes = methodList.size();
             p.println("  Input Methods: mMethodMapUpdateCount=" + mMethodMapUpdateCount);
             for (int i = 0; i < numImes; i++) {
-                InputMethodInfo info = mMethodList.get(i);
+                InputMethodInfo info = methodList.get(i);
                 p.println("  InputMethod #" + i + ":");
                 info.dump(p, "    ");
             }
@@ -6047,7 +6032,7 @@
             p.println("  mSwitchingController:");
             mSwitchingController.dump(p);
             p.println("  mSettings:");
-            mSettings.dumpLocked(p, "    ");
+            mSettings.dump(p, "    ");
 
             p.println("  mStartInputHistory:");
             mStartInputHistory.dump(pw, "    ");
@@ -6416,16 +6401,15 @@
         boolean failedToEnableUnknownIme = false;
         boolean previouslyEnabled = false;
         if (userId == mSettings.getCurrentUserId()) {
-            if (enabled && !mMethodMap.containsKey(imeId)) {
+            if (enabled && !mSettings.getMethodMap().containsKey(imeId)) {
                 failedToEnableUnknownIme = true;
             } else {
                 previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled);
             }
         } else {
-            final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-            final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
+            final InputMethodSettings settings = queryMethodMapForUser(userId);
             if (enabled) {
-                if (!methodMap.containsKey(imeId)) {
+                if (!settings.getMethodMap().containsKey(imeId)) {
                     failedToEnableUnknownIme = true;
                 } else {
                     final String enabledImeIdsStr = settings.getEnabledInputMethodsStr();
@@ -6438,9 +6422,9 @@
                 }
             } else {
                 previouslyEnabled =
-                        settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+                        settings.buildAndPutEnabledInputMethodsStrRemovingId(
                                 new StringBuilder(),
-                                settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId);
+                                settings.getEnabledInputMethodsAndSubtypeList(), imeId);
             }
         }
         if (failedToEnableUnknownIme) {
@@ -6537,9 +6521,9 @@
                         mBindingController.unbindCurrentMethod();
 
                         // Enable default IMEs, disable others
-                        var toDisable = mSettings.getEnabledInputMethodListLocked();
+                        var toDisable = mSettings.getEnabledInputMethodList();
                         var defaultEnabled = InputMethodInfoUtils.getDefaultEnabledImes(
-                                mContext, mMethodList);
+                                mContext, mSettings.getMethodList());
                         toDisable.removeAll(defaultEnabled);
                         for (InputMethodInfo info : toDisable) {
                             setInputMethodEnabledLocked(info.getId(), false);
@@ -6554,22 +6538,18 @@
                         updateInputMethodsFromSettingsLocked(true /* enabledMayChange */);
                         InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
                                 getPackageManagerForUser(mContext, mSettings.getCurrentUserId()),
-                                mSettings.getEnabledInputMethodListLocked());
+                                mSettings.getEnabledInputMethodList());
                         nextIme = mSettings.getSelectedInputMethod();
-                        nextEnabledImes = mSettings.getEnabledInputMethodListLocked();
+                        nextEnabledImes = mSettings.getEnabledInputMethodList();
                     } else {
-                        final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
-                        final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
                         final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
                                 new ArrayMap<>();
                         AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
-                        queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
-                                methodMap, methodList, DirectBootAwareness.AUTO);
-                        final InputMethodSettings settings = new InputMethodSettings(
-                                methodMap, userId);
+                        final InputMethodSettings settings = queryInputMethodServicesInternal(
+                                mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO);
 
                         nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext,
-                                methodList);
+                                settings.getMethodList());
                         nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME(
                                 nextEnabledImes).getId();
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMap.java b/services/core/java/com/android/server/inputmethod/InputMethodMap.java
new file mode 100644
index 0000000..a8e5e2e
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMap.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.annotation.AnyThread;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.view.inputmethod.InputMethodInfo;
+
+import java.util.List;
+
+/**
+ * A map from IME ID to {@link InputMethodInfo}, which is guaranteed to be immutable thus
+ * thread-safe.
+ */
+final class InputMethodMap {
+    private static final ArrayMap<String, InputMethodInfo> EMPTY_MAP =
+            new ArrayMap<>();
+
+    private final ArrayMap<String, InputMethodInfo> mMap;
+
+    static InputMethodMap emptyMap() {
+        return new InputMethodMap(EMPTY_MAP);
+    }
+
+    static InputMethodMap of(@NonNull ArrayMap<String, InputMethodInfo> map) {
+        return new InputMethodMap(map);
+    }
+
+    private InputMethodMap(@NonNull ArrayMap<String, InputMethodInfo> map) {
+        mMap = map.isEmpty() ? EMPTY_MAP : new ArrayMap<>(map);
+    }
+
+    @AnyThread
+    @Nullable
+    InputMethodInfo get(@Nullable String imeId) {
+        return mMap.get(imeId);
+    }
+
+    @AnyThread
+    @NonNull
+    List<InputMethodInfo> values() {
+        return List.copyOf(mMap.values());
+    }
+
+    @AnyThread
+    @Nullable
+    InputMethodInfo valueAt(int index) {
+        return mMap.valueAt(index);
+    }
+
+    @AnyThread
+    boolean containsKey(@Nullable String imeId) {
+        return mMap.containsKey(imeId);
+    }
+
+    @AnyThread
+    @IntRange(from = 0)
+    int size() {
+        return mMap.size();
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
index 9ddb428..abd7688 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
@@ -16,6 +16,7 @@
 
 package com.android.server.inputmethod;
 
+import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -58,7 +59,9 @@
     private static final char INPUT_METHOD_SUBTYPE_SEPARATOR =
             InputMethodUtils.INPUT_METHOD_SUBTYPE_SEPARATOR;
 
-    private final ArrayMap<String, InputMethodInfo> mMethodMap;
+    private final InputMethodMap mMethodMap;
+    private final List<InputMethodInfo> mMethodList;
+
     @UserIdInt
     private final int mCurrentUserId;
 
@@ -73,8 +76,17 @@
         }
     }
 
-    InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+    static InputMethodSettings createEmptyMap(@UserIdInt int userId) {
+        return new InputMethodSettings(InputMethodMap.emptyMap(), userId);
+    }
+
+    static InputMethodSettings create(InputMethodMap methodMap, @UserIdInt int userId) {
+        return new InputMethodSettings(methodMap, userId);
+    }
+
+    private InputMethodSettings(InputMethodMap methodMap, @UserIdInt int userId) {
         mMethodMap = methodMap;
+        mMethodList = methodMap.values();
         mCurrentUserId = userId;
         String ime = getSelectedInputMethod();
         String defaultDeviceIme = getSelectedDefaultDeviceInputMethod();
@@ -84,6 +96,18 @@
         }
     }
 
+    @AnyThread
+    @NonNull
+    InputMethodMap getMethodMap() {
+        return mMethodMap;
+    }
+
+    @AnyThread
+    @NonNull
+    List<InputMethodInfo> getMethodList() {
+        return mMethodList;
+    }
+
     private void putString(@NonNull String key, @Nullable String str) {
         SecureSettingsWrapper.putString(key, str, mCurrentUserId);
     }
@@ -101,32 +125,32 @@
         return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId);
     }
 
-    ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
-        return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */);
+    ArrayList<InputMethodInfo> getEnabledInputMethodList() {
+        return getEnabledInputMethodListWithFilter(null /* matchingCondition */);
     }
 
     @NonNull
-    ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked(
+    ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilter(
             @Nullable Predicate<InputMethodInfo> matchingCondition) {
-        return createEnabledInputMethodListLocked(
-                getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition);
+        return createEnabledInputMethodList(
+                getEnabledInputMethodsAndSubtypeList(), matchingCondition);
     }
 
-    List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
+    List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
             InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) {
         List<InputMethodSubtype> enabledSubtypes =
-                getEnabledInputMethodSubtypeListLocked(imi);
+                getEnabledInputMethodSubtypeList(imi);
         if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) {
-            enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+            enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypes(
                     SystemLocaleWrapper.get(mCurrentUserId), imi);
         }
         return InputMethodSubtype.sort(imi, enabledSubtypes);
     }
 
-    List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) {
-        final List<Pair<String, ArrayList<String>>> imsList =
-                getEnabledInputMethodsAndSubtypeListLocked();
-        final List<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
+    List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi) {
+        List<Pair<String, ArrayList<String>>> imsList =
+                getEnabledInputMethodsAndSubtypeList();
+        ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
         if (imi != null) {
             for (int i = 0; i < imsList.size(); ++i) {
                 final Pair<String, ArrayList<String>> imsPair = imsList.get(i);
@@ -149,7 +173,7 @@
         return enabledSubtypes;
     }
 
-    List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
+    List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeList() {
         final String enabledInputMethodsStr = getEnabledInputMethodsStr();
         final TextUtils.SimpleStringSplitter inputMethodSplitter =
                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
@@ -181,7 +205,7 @@
      *
      * @return the specified id was removed or not.
      */
-    boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+    boolean buildAndPutEnabledInputMethodsStrRemovingId(
             StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
         boolean isRemoved = false;
         boolean needsAppendSeparator = false;
@@ -209,7 +233,7 @@
         return isRemoved;
     }
 
-    private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
+    private ArrayList<InputMethodInfo> createEnabledInputMethodList(
             List<Pair<String, ArrayList<String>>> imsList,
             Predicate<InputMethodInfo> matchingCondition) {
         final ArrayList<InputMethodInfo> res = new ArrayList<>();
@@ -271,7 +295,7 @@
     }
 
     private void addSubtypeToHistory(String imeId, String subtypeId) {
-        final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
+        final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory();
         for (int i = 0; i < subtypeHistory.size(); ++i) {
             final Pair<String, String> ime = subtypeHistory.get(i);
             if (ime.first.equals(imeId)) {
@@ -303,14 +327,14 @@
         }
     }
 
-    Pair<String, String> getLastInputMethodAndSubtypeLocked() {
+    Pair<String, String> getLastInputMethodAndSubtype() {
         // Gets the first one from the history
-        return getLastSubtypeForInputMethodLockedInternal(null);
+        return getLastSubtypeForInputMethodInternal(null);
     }
 
     @Nullable
-    InputMethodSubtype getLastInputMethodSubtypeLocked() {
-        final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked();
+    InputMethodSubtype getLastInputMethodSubtype() {
+        final Pair<String, String> lastIme = getLastInputMethodAndSubtype();
         // TODO: Handle the case of the last IME with no subtypes
         if (lastIme == null || TextUtils.isEmpty(lastIme.first)
                 || TextUtils.isEmpty(lastIme.second)) {
@@ -331,8 +355,8 @@
         }
     }
 
-    String getLastSubtypeForInputMethodLocked(String imeId) {
-        Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
+    String getLastSubtypeForInputMethod(String imeId) {
+        Pair<String, String> ime = getLastSubtypeForInputMethodInternal(imeId);
         if (ime != null) {
             return ime.second;
         } else {
@@ -340,10 +364,10 @@
         }
     }
 
-    private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
+    private Pair<String, String> getLastSubtypeForInputMethodInternal(String imeId) {
         final List<Pair<String, ArrayList<String>>> enabledImes =
-                getEnabledInputMethodsAndSubtypeListLocked();
-        final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
+                getEnabledInputMethodsAndSubtypeList();
+        final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory();
         for (int i = 0; i < subtypeHistory.size(); ++i) {
             final Pair<String, String> imeAndSubtype = subtypeHistory.get(i);
             final String imeInTheHistory = imeAndSubtype.first;
@@ -351,7 +375,7 @@
             if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
                 final String subtypeInTheHistory = imeAndSubtype.second;
                 final String subtypeHashCode =
-                        getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
+                        getEnabledSubtypeHashCodeForInputMethodAndSubtype(
                                 enabledImes, imeInTheHistory, subtypeInTheHistory);
                 if (!TextUtils.isEmpty(subtypeHashCode)) {
                     if (DEBUG) {
@@ -368,7 +392,7 @@
         return null;
     }
 
-    private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
+    private String getEnabledSubtypeHashCodeForInputMethodAndSubtype(List<Pair<String,
             ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
         final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId);
         for (int i = 0; i < enabledImes.size(); ++i) {
@@ -383,7 +407,7 @@
                     // are enabled implicitly, so needs to treat them to be enabled.
                     if (imi != null && imi.getSubtypeCount() > 0) {
                         List<InputMethodSubtype> implicitlyEnabledSubtypes =
-                                SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList,
+                                SubtypeUtils.getImplicitlyApplicableSubtypes(localeList,
                                         imi);
                         final int numSubtypes = implicitlyEnabledSubtypes.size();
                         for (int j = 0; j < numSubtypes; ++j) {
@@ -420,7 +444,7 @@
         return null;
     }
 
-    private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
+    private List<Pair<String, String>> loadInputMethodAndSubtypeHistory() {
         ArrayList<Pair<String, String>> imsList = new ArrayList<>();
         final String subtypeHistoryStr = getSubtypeHistoryStr();
         if (TextUtils.isEmpty(subtypeHistoryStr)) {
@@ -583,7 +607,7 @@
         // If there are no selected subtypes, the framework will try to find the most applicable
         // subtype from explicitly or implicitly enabled subtypes.
         final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
-                getEnabledInputMethodSubtypeListLocked(imi, true);
+                getEnabledInputMethodSubtypeList(imi, true);
         // If there is only one explicitly or implicitly enabled subtype, just returns it.
         if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) {
             return null;
@@ -592,13 +616,13 @@
             return explicitlyOrImplicitlyEnabledSubtypes.get(0);
         }
         final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString();
-        final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+        final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtype(
                 explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD,
                 locale, true);
         if (subtype != null) {
             return subtype;
         }
-        return SubtypeUtils.findLastResortApplicableSubtypeLocked(
+        return SubtypeUtils.findLastResortApplicableSubtype(
                 explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
     }
 
@@ -690,7 +714,7 @@
         return sb.toString();
     }
 
-    void dumpLocked(final Printer pw, final String prefix) {
+    void dump(final Printer pw, final String prefix) {
         pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
     }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index 834ba20..1379d16 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -23,7 +23,6 @@
 import android.content.Context;
 import android.os.UserHandle;
 import android.text.TextUtils;
-import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Printer;
 import android.util.Slog;
@@ -158,15 +157,15 @@
 
     static List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
             boolean includeAuxiliarySubtypes, boolean isScreenLocked, boolean forImeMenu,
-            @NonNull Context context, @NonNull ArrayMap<String, InputMethodInfo> methodMap,
+            @NonNull Context context, @NonNull InputMethodMap methodMap,
             @UserIdInt int userId) {
         final Context userAwareContext = context.getUserId() == userId
                 ? context
                 : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
         final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag();
-        final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
+        final InputMethodSettings settings = InputMethodSettings.create(methodMap, userId);
 
-        final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodListLocked();
+        final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList();
         if (imis.isEmpty()) {
             return new ArrayList<>();
         }
@@ -184,7 +183,7 @@
                 continue;
             }
             final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList =
-                    settings.getEnabledInputMethodSubtypeListLocked(imi, true);
+                    settings.getEnabledInputMethodSubtypeList(imi, true);
             final ArraySet<String> enabledSubtypeSet = new ArraySet<>();
             for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) {
                 enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
@@ -479,7 +478,7 @@
     private ControllerImpl mController;
 
     private InputMethodSubtypeSwitchingController(@NonNull Context context,
-            @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+            @NonNull InputMethodMap methodMap, @UserIdInt int userId) {
         mContext = context;
         mUserId = userId;
         mController = ControllerImpl.createFrom(null,
@@ -491,7 +490,7 @@
     @NonNull
     public static InputMethodSubtypeSwitchingController createInstanceLocked(
             @NonNull Context context,
-            @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+            @NonNull InputMethodMap methodMap, @UserIdInt int userId) {
         return new InputMethodSubtypeSwitchingController(context, methodMap, userId);
     }
 
@@ -511,8 +510,7 @@
         mController.onUserActionLocked(imi, subtype);
     }
 
-    public void resetCircularListLocked(
-            @NonNull ArrayMap<String, InputMethodInfo> methodMap) {
+    public void resetCircularListLocked(@NonNull InputMethodMap methodMap) {
         mController = ControllerImpl.createFrom(mController,
                 getSortedInputMethodAndSubtypeList(
                         false /* includeAuxiliarySubtypes */, false /* isScreenLocked */,
diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
index 95df998..3d5c8677 100644
--- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
@@ -26,7 +26,6 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -52,7 +51,7 @@
             "EnabledWhenDefaultIsNotAsciiCapable";
 
     // A temporary workaround for the performance concerns in
-    // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo).
+    // #getImplicitlyApplicableSubtypes(Resources, InputMethodInfo).
     // TODO: Optimize all the critical paths including this one.
     // TODO(b/235661780): Make the cache supports multi-users.
     private static final Object sCacheLock = new Object();
@@ -121,9 +120,8 @@
     private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
             source -> source != null ? source.getLocaleObject() : null;
 
-    @VisibleForTesting
     @NonNull
-    static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
+    static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypes(
             @NonNull LocaleList systemLocales, InputMethodInfo imi) {
         synchronized (sCacheLock) {
             // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
@@ -133,11 +131,11 @@
             }
         }
 
-        // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
-        // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
+        // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesImpl().
+        // TODO: Refactor getImplicitlyApplicableSubtypesImpl() so that it can receive
         // LocaleList rather than Resource.
         final ArrayList<InputMethodSubtype> result =
-                getImplicitlyApplicableSubtypesLockedImpl(systemLocales, imi);
+                getImplicitlyApplicableSubtypesImpl(systemLocales, imi);
         synchronized (sCacheLock) {
             // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
             sCachedSystemLocales = systemLocales;
@@ -147,7 +145,7 @@
         return result;
     }
 
-    private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
+    private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesImpl(
             @NonNull LocaleList systemLocales, InputMethodInfo imi) {
         final List<InputMethodSubtype> subtypes = getSubtypes(imi);
         final String systemLocale = systemLocales.get(0).toString();
@@ -215,7 +213,7 @@
         }
 
         if (applicableSubtypes.isEmpty()) {
-            InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
+            InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtype(
                     subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
             if (lastResortKeyboardSubtype != null) {
                 applicableSubtypes.add(lastResortKeyboardSubtype);
@@ -244,7 +242,7 @@
      *
      * @return the most applicable subtypeId
      */
-    static InputMethodSubtype findLastResortApplicableSubtypeLocked(
+    static InputMethodSubtype findLastResortApplicableSubtype(
             List<InputMethodSubtype> subtypes, String mode, @NonNull String locale,
             boolean canIgnoreLocaleAsLastResort) {
         if (subtypes == null || subtypes.isEmpty()) {
diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
index 9c4225d..39df5be 100644
--- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
@@ -1384,7 +1384,8 @@
         try {
             reportLocation(LocationResult.wrap(location).validate());
         } catch (BadLocationException e) {
-            throw new IllegalArgumentException(e);
+            Log.e(TAG, "Dropping invalid location: " + e);
+            return;
         }
 
         if (mStarted) {
@@ -1759,7 +1760,8 @@
             try {
                 reportLocation(LocationResult.wrap(locations).validate());
             } catch (BadLocationException e) {
-                throw new IllegalArgumentException(e);
+                Log.e(TAG, "Dropping invalid locations: " + e);
+                return;
             }
         }
 
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 28a1c7a..85a1315 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -2336,6 +2336,11 @@
                     mSystemProvider.getDefaultRoute());
         }
 
+        private static String getPackageNameFromNullableRecord(
+                @Nullable RouterRecord routerRecord) {
+            return routerRecord != null ? routerRecord.mPackageName : "<null router record>";
+        }
+
         private static String toLoggingMessage(
                 String source, String providerId, ArrayList<MediaRoute2Info> routes) {
             String routesString =
@@ -2573,8 +2578,17 @@
 
             RouterRecord matchingRecord = mSessionToRouterMap.get(uniqueSessionId);
             if (matchingRecord != routerRecord) {
-                Slog.w(TAG, "Ignoring " + description + " route from non-matching router. "
-                        + "packageName=" + routerRecord.mPackageName + " route=" + route);
+                Slog.w(
+                        TAG,
+                        "Ignoring "
+                                + description
+                                + " route from non-matching router."
+                                + " routerRecordPackageName="
+                                + getPackageNameFromNullableRecord(routerRecord)
+                                + " matchingRecordPackageName="
+                                + getPackageNameFromNullableRecord(matchingRecord)
+                                + " route="
+                                + route);
                 return false;
             }
 
@@ -2613,9 +2627,15 @@
                 @Nullable RouterRecord routerRecord, @NonNull String uniqueSessionId) {
             final RouterRecord matchingRecord = mSessionToRouterMap.get(uniqueSessionId);
             if (matchingRecord != routerRecord) {
-                Slog.w(TAG, "Ignoring releasing session from non-matching router. packageName="
-                        + (routerRecord == null ? null : routerRecord.mPackageName)
-                        + " uniqueSessionId=" + uniqueSessionId);
+                Slog.w(
+                        TAG,
+                        "Ignoring releasing session from non-matching router."
+                                + " routerRecordPackageName="
+                                + getPackageNameFromNullableRecord(routerRecord)
+                                + " matchingRecordPackageName="
+                                + getPackageNameFromNullableRecord(matchingRecord)
+                                + " uniqueSessionId="
+                                + uniqueSessionId);
                 return;
             }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 308d441..425659e 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -282,6 +282,7 @@
 import android.service.notification.StatusBarNotification;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeProto;
+import android.service.notification.ZenPolicy;
 import android.telecom.TelecomManager;
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
@@ -5968,6 +5969,20 @@
             }
         }
 
+        /**
+         * Gets the device-default zen policy as a ZenPolicy.
+         */
+        @Override
+        public ZenPolicy getDefaultZenPolicy() {
+            enforceSystemOrSystemUI("INotificationManager.getDefaultZenPolicy");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                return mZenModeHelper.getDefaultZenPolicy();
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
         @Override
         public List<String> getEnabledNotificationListenerPackages() {
             checkCallerIsSystem();
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 153af13..c067fa0 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -604,6 +604,14 @@
                 ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
                 if (rule == null) {
                     rule = newImplicitZenRule(callingPkg);
+
+                    // For new implicit rules, create a policy matching the current global
+                    // (manual rule) settings, for consistency with the policy that
+                    // would apply if changing the global interruption filter. We only do this
+                    // for newly created rules, as existing rules have a pre-existing policy
+                    // (whether initialized here or set via app or user).
+                    rule.zenPolicy = mConfig.toZenPolicy();
+
                     newConfig.automaticRules.put(rule.id, rule);
                 }
                 // If the user has changed the rule's *zenMode*, then don't let app overwrite it.
@@ -615,6 +623,7 @@
                 rule.condition = new Condition(rule.conditionId,
                         mContext.getString(R.string.zen_mode_implicit_activated),
                         Condition.STATE_TRUE);
+
                 setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP,
                         "applyGlobalZenModeAsImplicitZenRule", callingUid);
             }
@@ -643,8 +652,10 @@
                 return;
             }
             ZenModeConfig newConfig = mConfig.copy();
+            boolean isNew = false;
             ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
             if (rule == null) {
+                isNew = true;
                 rule = newImplicitZenRule(callingPkg);
                 rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
                 newConfig.automaticRules.put(rule.id, rule);
@@ -652,10 +663,20 @@
             // If the user has changed the rule's *ZenPolicy*, then don't let app overwrite it.
             // We allow the update if the user has only changed other aspects of the rule.
             if (rule.zenPolicyUserModifiedFields == 0) {
+                ZenPolicy newZenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
+                if (isNew) {
+                    // For new rules only, fill anything underspecified in the new policy with
+                    // values from the global configuration, for consistency with the policy that
+                    // would take effect if changing the global policy.
+                    // Note that NotificationManager.Policy cannot have any unset priority
+                    // categories, but *can* have unset visual effects, which is why we do this.
+                    newZenPolicy = mConfig.toZenPolicy().overwrittenWith(newZenPolicy);
+                }
                 updatePolicy(
                         rule,
-                        ZenAdapters.notificationPolicyToZenPolicy(policy),
-                        /* updateBitmask= */ false);
+                        newZenPolicy,
+                        /* updateBitmask= */ false,
+                        isNew);
 
                 setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP,
                         "applyGlobalPolicyAsImplicitZenRule", callingUid);
@@ -685,6 +706,11 @@
             }
             ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg));
             if (implicitRule != null && implicitRule.zenPolicy != null) {
+                // toNotificationPolicy takes defaults from mConfig, and technically, those are not
+                // the defaults that would apply if any fields were unset. However, all rules should
+                // have all fields set in their ZenPolicy objects upon rule creation, so in
+                // practice, this is only filling in the areChannelsBypassingDnd field, which is a
+                // state rather than a part of the policy.
                 return mConfig.toNotificationPolicy(implicitRule.zenPolicy);
             } else {
                 return getNotificationPolicy();
@@ -1072,7 +1098,8 @@
             rule.zenMode = newZenMode;
 
             // Updates the bitmask and values for all policy fields, based on the origin.
-            updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask);
+            updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask, isNew);
+
             // Updates the bitmask and values for all device effect fields, based on the origin.
             updateZenDeviceEffects(rule, automaticZenRule.getDeviceEffects(),
                     origin == UPDATE_ORIGIN_APP, updateBitmask);
@@ -1111,14 +1138,19 @@
     /**
      * Modifies the {@link ZenPolicy} associated to a new or updated ZenRule.
      *
-     * <p>The new policy is {@code newPolicy}, while the user-modified bitmask is updated to reflect
-     * the changes being applied (if applicable, i.e. if the update is from the user).
+     * <p>The update takes any set fields in {@code newPolicy} as new policy settings for the
+     * provided {@code ZenRule}, keeping any pre-existing settings from {@code zenRule.zenPolicy}
+     * for any unset policy fields in {@code newPolicy}. The user-modified bitmask is updated to
+     * reflect the changes being applied (if applicable, i.e. if the update is from the user).
      */
     private void updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy,
-            boolean updateBitmask) {
+            boolean updateBitmask, boolean isNew) {
         if (newPolicy == null) {
-            // TODO: b/319242206 - Treat as newPolicy == default policy and continue below.
-            zenRule.zenPolicy = null;
+            if (isNew) {
+                // Newly created rule with no provided policy; fill in with the default.
+                zenRule.zenPolicy = mDefaultConfig.toZenPolicy();
+            }
+            // Otherwise, a null policy means no policy changes, so we can stop here.
             return;
         }
 
@@ -1127,6 +1159,16 @@
         ZenPolicy oldPolicy =
                 zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy();
 
+        // If this is updating a rule rather than creating a new one, keep any fields from the
+        // old policy if they are unspecified in the new policy. For newly created rules, oldPolicy
+        // has been set to the default settings above, so any unspecified fields in a newly created
+        // policy are filled with default values. Then use the fully-specified version of the new
+        // policy for comparison below.
+        //
+        // Although we do not expect a policy update from the user to contain any unset fields,
+        // filling in fields here also guards against any unset fields counting as a "diff" when
+        // comparing fields for bitmask editing below.
+        newPolicy = oldPolicy.overwrittenWith(newPolicy);
         zenRule.zenPolicy = newPolicy;
 
         if (updateBitmask) {
@@ -1452,11 +1494,27 @@
                     }
 
                     allRulesDisabled &= !automaticRule.enabled;
+
+                    // Upon upgrading to a version with modes_api enabled, keep all behaviors of
+                    // rules with null ZenPolicies explicitly as a copy of the global policy.
+                    if (Flags.modesApi() && config.version < ZenModeConfig.XML_VERSION_MODES_API) {
+                        // Keep the manual ("global") policy that from config.
+                        ZenPolicy manualRulePolicy = config.toZenPolicy();
+                        if (automaticRule.zenPolicy == null) {
+                            automaticRule.zenPolicy = manualRulePolicy;
+                        } else {
+                            // newPolicy is a policy with all unset fields in the rule's zenPolicy
+                            // set to their values from the values in config. Then convert that back
+                            // to ZenPolicy to store with the automatic zen rule.
+                            automaticRule.zenPolicy =
+                                    manualRulePolicy.overwrittenWith(automaticRule.zenPolicy);
+                        }
+                    }
                 }
             }
 
             if (!hasDefaultRules && allRulesDisabled
-                    && (forRestore || config.version < ZenModeConfig.XML_VERSION)) {
+                    && (forRestore || config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE)) {
                 // reset zen automatic rules to default on restore or upgrade if:
                 // - doesn't already have default rules and
                 // - all previous automatic rules were disabled
@@ -1473,7 +1531,7 @@
 
             // Resolve user id for settings.
             userId = userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId;
-            if (config.version < ZenModeConfig.XML_VERSION) {
+            if (config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE) {
                 Settings.Secure.putIntForUser(mContext.getContentResolver(),
                         Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 1, userId);
             } else {
@@ -1600,6 +1658,14 @@
         return mConsolidatedPolicy.copy();
     }
 
+    /**
+     * Returns a copy of the device default policy as a ZenPolicy object.
+     */
+    @VisibleForTesting
+    protected ZenPolicy getDefaultZenPolicy() {
+        return mDefaultConfig.toZenPolicy();
+    }
+
     @GuardedBy("mConfigLock")
     private boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent,
             @ConfigChangeOrigin int origin, String reason, int callingUid) {
@@ -1783,7 +1849,7 @@
     }
 
     @GuardedBy("mConfigLock")
-    private void applyCustomPolicy(ZenPolicy policy, ZenRule rule) {
+    private void applyCustomPolicy(ZenPolicy policy, ZenRule rule, boolean useManualConfig) {
         if (rule.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) {
             policy.apply(new ZenPolicy.Builder()
                     .disallowAllSounds()
@@ -1797,8 +1863,22 @@
         } else if (rule.zenPolicy != null) {
             policy.apply(rule.zenPolicy);
         } else {
-            // active rule with no specified policy inherits the default settings
-            policy.apply(mConfig.toZenPolicy());
+            if (Flags.modesApi()) {
+                if (useManualConfig) {
+                    // manual rule is configured using the settings stored directly in mConfig
+                    policy.apply(mConfig.toZenPolicy());
+                } else {
+                    // under modes_api flag, an active automatic rule with no specified policy
+                    // inherits the device default settings as stored in mDefaultConfig. While the
+                    // rule's policy fields should be set upon creation, this is a fallback to
+                    // catch any that may have fallen through the cracks.
+                    Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule);
+                    policy.apply(mDefaultConfig.toZenPolicy());
+                }
+            } else {
+                // active rule with no specified policy inherits the global config settings
+                policy.apply(mConfig.toZenPolicy());
+            }
         }
     }
 
@@ -1810,7 +1890,7 @@
             ZenPolicy policy = new ZenPolicy();
             ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder();
             if (mConfig.manualRule != null) {
-                applyCustomPolicy(policy, mConfig.manualRule);
+                applyCustomPolicy(policy, mConfig.manualRule, true);
                 if (Flags.modesApi()) {
                     deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects);
                 }
@@ -1822,7 +1902,7 @@
                     // policy. This is relevant in case some other active rule has a more
                     // restrictive INTERRUPTION_FILTER but a more lenient ZenPolicy!
                     if (!Flags.modesApi() || automaticRule.zenMode != Global.ZEN_MODE_OFF) {
-                        applyCustomPolicy(policy, automaticRule);
+                        applyCustomPolicy(policy, automaticRule, false);
                     }
                     if (Flags.modesApi()) {
                         deviceEffectsBuilder.add(automaticRule.zenDeviceEffects);
@@ -1830,6 +1910,14 @@
                 }
             }
 
+            // While mConfig.toNotificationPolicy fills in any unset fields from the provided
+            // config (which, in this case is the manual "global" config), under modes API changes,
+            // we should have no remaining unset fields: the manual policy gets every field from
+            // the global policy, and each automatic rule has all policy fields filled in on
+            // creation or update.
+            // However, the piece of information that comes from mConfig that we must keep is the
+            // areChannelsBypassingDnd bit, which is a state, rather than a policy, and even when
+            // all policy fields are set, this state comes to the new policy from this call.
             Policy newPolicy = mConfig.toNotificationPolicy(policy);
             if (!Objects.equals(mConsolidatedPolicy, newPolicy)) {
                 mConsolidatedPolicy = newPolicy;
diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java
index 25a39cc..86d05d9 100644
--- a/services/core/java/com/android/server/om/IdmapManager.java
+++ b/services/core/java/com/android/server/om/IdmapManager.java
@@ -257,7 +257,7 @@
     private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage,
             @NonNull AndroidPackage overlayPackage, int userId) {
         String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName();
-        if (targetOverlayableName != null) {
+        if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) {
             try {
                 OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget(
                         targetPackage.getPackageName(), targetOverlayableName, userId);
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index b9464d9..a61b03f 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -32,6 +32,7 @@
 import static android.os.Trace.TRACE_TAG_RRO;
 import static android.os.Trace.traceBegin;
 import static android.os.Trace.traceEnd;
+
 import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException;
 
 import android.annotation.NonNull;
@@ -362,7 +363,7 @@
                 defaultPackages.add(packageName);
             }
         }
-        return defaultPackages.toArray(new String[defaultPackages.size()]);
+        return defaultPackages.toArray(new String[0]);
     }
 
     private final class OverlayManagerPackageMonitor extends PackageMonitor {
@@ -1143,9 +1144,10 @@
     };
 
     private static final class PackageManagerHelperImpl implements PackageManagerHelper {
-        private static class PackageStateUsers {
+        private static final class PackageStateUsers {
             private PackageState mPackageState;
-            private final Set<Integer> mInstalledUsers = new ArraySet<>();
+            private Boolean mDefinesOverlayable = null;
+            private final ArraySet<Integer> mInstalledUsers = new ArraySet<>();
             private PackageStateUsers(@NonNull PackageState packageState) {
                 this.mPackageState = packageState;
             }
@@ -1160,7 +1162,7 @@
         // state may lead to contradictions within OMS. Better then to lag
         // behind until all pending intents have been processed.
         private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>();
-        private final Set<Integer> mInitializedUsers = new ArraySet<>();
+        private final ArraySet<Integer> mInitializedUsers = new ArraySet<>();
 
         PackageManagerHelperImpl(Context context) {
             mContext = context;
@@ -1176,8 +1178,7 @@
          */
         @NonNull
         public ArrayMap<String, PackageState> initializeForUser(final int userId) {
-            if (!mInitializedUsers.contains(userId)) {
-                mInitializedUsers.add(userId);
+            if (mInitializedUsers.add(userId)) {
                 mPackageManagerInternal.forEachPackageState((packageState -> {
                     if (packageState.getPkg() != null
                             && packageState.getUserStateOrDefault(userId).isInstalled()) {
@@ -1196,13 +1197,11 @@
             return userPackages;
         }
 
-        @Override
-        @Nullable
-        public PackageState getPackageStateForUser(@NonNull final String packageName,
+        private PackageStateUsers getRawPackageStateForUser(@NonNull final String packageName,
                 final int userId) {
             final PackageStateUsers pkg = mCache.get(packageName);
             if (pkg != null && pkg.mInstalledUsers.contains(userId)) {
-                return pkg.mPackageState;
+                return pkg;
             }
             try {
                 if (!mPackageManager.isPackageAvailable(packageName, userId)) {
@@ -1216,8 +1215,14 @@
             return addPackageUser(packageName, userId);
         }
 
-        @NonNull
-        private PackageState addPackageUser(@NonNull final String packageName,
+        @Override
+        public PackageState getPackageStateForUser(@NonNull final String packageName,
+                final int userId) {
+            final PackageStateUsers pkg = getRawPackageStateForUser(packageName, userId);
+            return pkg != null ? pkg.mPackageState : null;
+        }
+
+        private PackageStateUsers addPackageUser(@NonNull final String packageName,
                 final int user) {
             final PackageState pkg = mPackageManagerInternal.getPackageStateInternal(packageName);
             if (pkg == null) {
@@ -1229,20 +1234,20 @@
         }
 
         @NonNull
-        private PackageState addPackageUser(@NonNull final PackageState pkg,
+        private PackageStateUsers addPackageUser(@NonNull final PackageState pkg,
                 final int user) {
             PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName());
             if (pkgUsers == null) {
                 pkgUsers = new PackageStateUsers(pkg);
                 mCache.put(pkg.getPackageName(), pkgUsers);
-            } else {
+            } else if (pkgUsers.mPackageState != pkg) {
                 pkgUsers.mPackageState = pkg;
+                pkgUsers.mDefinesOverlayable = null;
             }
             pkgUsers.mInstalledUsers.add(user);
-            return pkgUsers.mPackageState;
+            return pkgUsers;
         }
 
-
         @NonNull
         private void removePackageUser(@NonNull final String packageName, final int user) {
             final PackageStateUsers pkgUsers = mCache.get(packageName);
@@ -1260,15 +1265,15 @@
             }
         }
 
-        @Nullable
         public PackageState onPackageAdded(@NonNull final String packageName, final int userId) {
-            return addPackageUser(packageName, userId);
+            final var pu = addPackageUser(packageName, userId);
+            return pu != null ? pu.mPackageState : null;
         }
 
-        @Nullable
         public PackageState onPackageUpdated(@NonNull final String packageName,
                 final int userId) {
-            return addPackageUser(packageName, userId);
+            final var pu = addPackageUser(packageName, userId);
+            return pu != null ? pu.mPackageState : null;
         }
 
         public void onPackageRemoved(@NonNull final String packageName, final int userId) {
@@ -1308,22 +1313,30 @@
             return (pkgs.length == 0) ? null : pkgs[0];
         }
 
-        @Nullable
         @Override
         public OverlayableInfo getOverlayableForTarget(@NonNull String packageName,
                 @NonNull String targetOverlayableName, int userId)
                 throws IOException {
-            var packageState = getPackageStateForUser(packageName, userId);
-            var pkg = packageState == null ? null : packageState.getAndroidPackage();
+            final var psu = getRawPackageStateForUser(packageName, userId);
+            final var pkg = (psu == null || psu.mPackageState == null)
+                    ? null : psu.mPackageState.getAndroidPackage();
             if (pkg == null) {
                 throw new IOException("Unable to get target package");
             }
 
+            if (Boolean.FALSE.equals(psu.mDefinesOverlayable)) {
+                return null;
+            }
+
             ApkAssets apkAssets = null;
             try {
                 apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(),
                         ApkAssets.PROPERTY_ONLY_OVERLAYABLES);
-                return apkAssets.getOverlayableInfo(targetOverlayableName);
+                if (psu.mDefinesOverlayable == null) {
+                    psu.mDefinesOverlayable = apkAssets.definesOverlayable();
+                }
+                return Boolean.FALSE.equals(psu.mDefinesOverlayable)
+                        ? null : apkAssets.getOverlayableInfo(targetOverlayableName);
             } finally {
                 if (apkAssets != null) {
                     try {
@@ -1337,24 +1350,29 @@
         @Override
         public boolean doesTargetDefineOverlayable(String targetPackageName, int userId)
                 throws IOException {
-            var packageState = getPackageStateForUser(targetPackageName, userId);
-            var pkg = packageState == null ? null : packageState.getAndroidPackage();
+            final var psu = getRawPackageStateForUser(targetPackageName, userId);
+            var pkg = (psu == null || psu.mPackageState == null)
+                    ? null : psu.mPackageState.getAndroidPackage();
             if (pkg == null) {
                 throw new IOException("Unable to get target package");
             }
 
-            ApkAssets apkAssets = null;
-            try {
-                apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath());
-                return apkAssets.definesOverlayable();
-            } finally {
-                if (apkAssets != null) {
-                    try {
-                        apkAssets.close();
-                    } catch (Throwable ignored) {
+            if (psu.mDefinesOverlayable == null) {
+                ApkAssets apkAssets = null;
+                try {
+                    apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(),
+                            ApkAssets.PROPERTY_ONLY_OVERLAYABLES);
+                    psu.mDefinesOverlayable = apkAssets.definesOverlayable();
+                } finally {
+                    if (apkAssets != null) {
+                        try {
+                            apkAssets.close();
+                        } catch (Throwable ignored) {
+                        }
                     }
                 }
             }
+            return psu.mDefinesOverlayable;
         }
 
         @Override
@@ -1545,8 +1563,7 @@
                 final OverlayPaths frameworkOverlays =
                         mImpl.getEnabledOverlayPaths("android", userId, false);
                 for (final String targetPackageName : targetPackageNames) {
-                    final OverlayPaths.Builder list = new OverlayPaths.Builder();
-                    list.addAll(frameworkOverlays);
+                    final var list = new OverlayPaths.Builder(frameworkOverlays);
                     if (!"android".equals(targetPackageName)) {
                         list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true));
                     }
@@ -1558,17 +1575,21 @@
             final HashSet<String> invalidPackages = new HashSet<>();
             pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages);
 
-            for (final String targetPackageName : targetPackageNames) {
-                if (DEBUG) {
-                    Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=["
-                            + pendingChanges.get(targetPackageName)
-                            + "] userId=" + userId);
-                }
+            if (DEBUG || !invalidPackages.isEmpty()) {
+                for (final String targetPackageName : targetPackageNames) {
+                    if (DEBUG) {
+                        Slog.d(TAG,
+                                "-> Updating overlay: target=" + targetPackageName + " overlays=["
+                                        + pendingChanges.get(targetPackageName)
+                                        + "] userId=" + userId);
+                    }
 
-                if (invalidPackages.contains(targetPackageName)) {
-                    Slog.e(TAG, TextUtils.formatSimple(
-                            "Failed to change enabled overlays for %s user %d", targetPackageName,
-                            userId));
+                    if (invalidPackages.contains(targetPackageName)) {
+                        Slog.e(TAG, TextUtils.formatSimple(
+                                "Failed to change enabled overlays for %s user %d",
+                                targetPackageName,
+                                userId));
+                    }
                 }
             }
             return new ArrayList<>(updatedPackages);
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 972c78d..c1b6ccc 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -772,24 +772,20 @@
 
     OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName,
             final int userId, boolean includeImmutableOverlays) {
-        final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName,
-                userId);
-        final OverlayPaths.Builder paths = new OverlayPaths.Builder();
-        final int n = overlays.size();
-        for (int i = 0; i < n; i++) {
-            final OverlayInfo oi = overlays.get(i);
+        final var paths = new OverlayPaths.Builder();
+        mSettings.forEachMatching(userId, null, targetPackageName, oi -> {
             if (!oi.isEnabled()) {
-                continue;
+                return;
             }
             if (!includeImmutableOverlays && !oi.isMutable) {
-                continue;
+                return;
             }
             if (oi.isFabricated()) {
                 paths.addNonApkPath(oi.baseCodePath);
             } else {
                 paths.addApkPath(oi.baseCodePath);
             }
-        }
+        });
         return paths.build();
     }
 
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index eae614a..b8b49f3e 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -47,6 +47,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
@@ -182,6 +183,23 @@
         return CollectionUtils.map(items, SettingsItem::getOverlayInfo);
     }
 
+    void forEachMatching(int userId, String overlayName, String targetPackageName,
+            @NonNull Consumer<OverlayInfo> consumer) {
+        for (int i = 0, n = mItems.size(); i < n; i++) {
+            final SettingsItem item = mItems.get(i);
+            if (item.getUserId() != userId) {
+                continue;
+            }
+            if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) {
+                continue;
+            }
+            if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) {
+                continue;
+            }
+            consumer.accept(item.getOverlayInfo());
+        }
+    }
+
     ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
         final List<SettingsItem> items = selectWhereUser(userId);
 
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index f3df424..cc4c2b5 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -1031,12 +1031,12 @@
     private void recomputeComponentVisibility(
             ArrayMap<String, ? extends PackageStateInternal> existingSettings) {
         final WatchedArraySet<String> protectedBroadcasts;
-        final WatchedArraySet<Integer> forceQueryable;
+        final ArraySet<Integer> forceQueryable;
         synchronized (mProtectedBroadcastsLock) {
-            protectedBroadcasts = mProtectedBroadcasts.snapshot();
+            protectedBroadcasts = new WatchedArraySet<String>(mProtectedBroadcasts);
         }
         synchronized (mForceQueryableLock) {
-            forceQueryable = mForceQueryable.snapshot();
+            forceQueryable = new ArraySet<Integer>(mForceQueryable.untrackedStorage());
         }
         final ParallelComputeComponentVisibility computer = new ParallelComputeComponentVisibility(
                 existingSettings, forceQueryable, protectedBroadcasts);
diff --git a/services/core/java/com/android/server/pm/AppsFilterUtils.java b/services/core/java/com/android/server/pm/AppsFilterUtils.java
index 200734b..a02a1bc 100644
--- a/services/core/java/com/android/server/pm/AppsFilterUtils.java
+++ b/services/core/java/com/android/server/pm/AppsFilterUtils.java
@@ -198,12 +198,12 @@
         private static final int MAX_THREADS = 4;
 
         private final ArrayMap<String, ? extends PackageStateInternal> mExistingSettings;
-        private final WatchedArraySet<Integer> mForceQueryable;
+        private final ArraySet<Integer> mForceQueryable;
         private final WatchedArraySet<String> mProtectedBroadcasts;
 
         ParallelComputeComponentVisibility(
                 @NonNull ArrayMap<String, ? extends PackageStateInternal> existingSettings,
-                @NonNull WatchedArraySet<Integer> forceQueryable,
+                @NonNull ArraySet<Integer> forceQueryable,
                 @NonNull WatchedArraySet<String> protectedBroadcasts) {
             mExistingSettings = existingSettings;
             mForceQueryable = forceQueryable;
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index c110fb6..200b17b 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -16,7 +16,12 @@
 
 package com.android.server.pm;
 
+import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.app.Flags;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
@@ -27,6 +32,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Environment;
 import android.os.Handler;
@@ -69,26 +75,29 @@
     private static final String DISK_FILE_NAME = "states";
     private static final String DISK_DIR_NAME = "bic";
 
-    private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10;
+    private static final String ENFORCE_PERMISSION_ERROR_MSG =
+            "User is not permitted to call service: ";
 
+    private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10;
     private static final int MSG_USAGE_EVENT_RECEIVED = 0;
     private static final int MSG_PACKAGE_ADDED = 1;
     private static final int MSG_PACKAGE_REMOVED = 2;
 
     private final BinderService mBinderService;
     private final PackageManager mPackageManager;
+    // TODO migrate all internal PackageManager calls to PackageManagerInternal where possible.
+    // b/310983905
     private final PackageManagerInternal mPackageManagerInternal;
     private final PermissionManagerServiceInternal mPermissionManager;
     private final Handler mHandler;
     private final File mDiskFile;
-
+    private final Context mContext;
 
     private SparseSetArray<String> mBackgroundInstalledPackages = null;
 
     // User ID -> package name -> set of foreground time frame
-    private final SparseArrayMap<String,
-            TreeSet<ForegroundTimeFrame>> mInstallerForegroundTimeFrames =
-            new SparseArrayMap<>();
+    private final SparseArrayMap<String, TreeSet<ForegroundTimeFrame>>
+            mInstallerForegroundTimeFrames = new SparseArrayMap<>();
 
     public BackgroundInstallControlService(@NonNull Context context) {
         this(new InjectorImpl(context));
@@ -102,15 +111,13 @@
         mPermissionManager = injector.getPermissionManager();
         mHandler = new EventHandler(injector.getLooper(), this);
         mDiskFile = injector.getDiskFile();
+        mContext = injector.getContext();
         UsageStatsManagerInternal usageStatsManagerInternal =
                 injector.getUsageStatsManagerInternal();
         usageStatsManagerInternal.registerListener(
                 (userId, event) ->
-                        mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED,
-                                userId,
-                                0,
-                                event).sendToTarget()
-        );
+                        mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED, userId, 0, event)
+                                .sendToTarget());
         mBinderService = new BinderService(this);
     }
 
@@ -124,12 +131,17 @@
         @Override
         public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
                 @PackageManager.PackageInfoFlagsBits long flags, int userId) {
+            if (Flags.bicClient()) {
+                mService.enforceCallerPermissions();
+            }
             if (!Build.IS_DEBUGGABLE) {
                 return mService.getBackgroundInstalledPackages(flags, userId);
             }
             // The debug.transparency.bg-install-apps (only works for debuggable builds)
             // is used to set mock list of background installed apps for testing.
             // The list of apps' names is delimited by ",".
+            // TODO: Remove after migrating test to new background install method using
+            // {@link BackgroundInstallControlCallbackHelperTest}.installPackage b/310983905
             String propertyString = SystemProperties.get("debug.transparency.bg-install-apps");
             if (TextUtils.isEmpty(propertyString)) {
                 return mService.getBackgroundInstalledPackages(flags, userId);
@@ -137,25 +149,36 @@
                 return mService.getMockBackgroundInstalledPackages(propertyString);
             }
         }
+
+    }
+
+    @RequiresPermission(GET_BACKGROUND_INSTALLED_PACKAGES)
+    void enforceCallerPermissions() throws SecurityException {
+        mContext.enforceCallingOrSelfPermission(GET_BACKGROUND_INSTALLED_PACKAGES,
+                ENFORCE_PERMISSION_ERROR_MSG + GET_BACKGROUND_INSTALLED_PACKAGES);
     }
 
     @VisibleForTesting
     ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
             @PackageManager.PackageInfoFlagsBits long flags, int userId) {
-        List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
+        final long token = Binder.clearCallingIdentity();
+        try {
+            List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
                     PackageManager.PackageInfoFlags.of(flags), userId);
 
-        initBackgroundInstalledPackages();
-
-        ListIterator<PackageInfo> iter = packages.listIterator();
-        while (iter.hasNext()) {
-            String packageName = iter.next().packageName;
-            if (!mBackgroundInstalledPackages.contains(userId, packageName)) {
-                iter.remove();
+            initBackgroundInstalledPackages();
+            ListIterator<PackageInfo> iter = packages.listIterator();
+            while (iter.hasNext()) {
+                String packageName = iter.next().packageName;
+                if (!mBackgroundInstalledPackages.contains(userId, packageName)) {
+                    iter.remove();
+                }
             }
-        }
 
-        return new ParceledListSlice<>(packages);
+            return new ParceledListSlice<>(packages);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
     }
 
     /**
@@ -168,8 +191,9 @@
         List<PackageInfo> mockPackages = new ArrayList<>();
         for (String name : mockPackageNames) {
             try {
-                PackageInfo packageInfo = mPackageManager.getPackageInfo(name,
-                        PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL));
+                PackageInfo packageInfo =
+                        mPackageManager.getPackageInfo(
+                                name, PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL));
                 mockPackages.add(packageInfo);
             } catch (PackageManager.NameNotFoundException e) {
                 Slog.w(TAG, "Package's PackageInfo not found " + name);
@@ -190,18 +214,16 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_USAGE_EVENT_RECEIVED: {
-                    mService.handleUsageEvent((UsageEvents.Event) msg.obj, msg.arg1 /* userId */);
+                case MSG_USAGE_EVENT_RECEIVED:
+                    mService.handleUsageEvent(
+                            (UsageEvents.Event) msg.obj, msg.arg1 /* userId */);
                     break;
-                }
-                case MSG_PACKAGE_ADDED: {
+                case MSG_PACKAGE_ADDED:
                     mService.handlePackageAdd((String) msg.obj, msg.arg1 /* userId */);
                     break;
-                }
-                case MSG_PACKAGE_REMOVED: {
+                case MSG_PACKAGE_REMOVED:
                     mService.handlePackageRemove((String) msg.obj, msg.arg1 /* userId */);
                     break;
-                }
                 default:
                     Slog.w(TAG, "Unknown message: " + msg.what);
             }
@@ -211,8 +233,9 @@
     void handlePackageAdd(String packageName, int userId) {
         ApplicationInfo appInfo = null;
         try {
-            appInfo = mPackageManager.getApplicationInfoAsUser(packageName,
-                    PackageManager.ApplicationInfoFlags.of(0), userId);
+            appInfo =
+                    mPackageManager.getApplicationInfoAsUser(
+                            packageName, PackageManager.ApplicationInfoFlags.of(0), userId);
         } catch (PackageManager.NameNotFoundException e) {
             Slog.w(TAG, "Package's appInfo not found " + packageName);
             return;
@@ -231,15 +254,18 @@
 
         // the installers without INSTALL_PACKAGES perm can't perform
         // the installation in background. So we can just filter out them.
-        if (mPermissionManager.checkPermission(installerPackageName,
-                android.Manifest.permission.INSTALL_PACKAGES, Context.DEVICE_ID_DEFAULT,
-                userId) != PackageManager.PERMISSION_GRANTED) {
+        if (mPermissionManager.checkPermission(
+                installerPackageName,
+                android.Manifest.permission.INSTALL_PACKAGES,
+                Context.DEVICE_ID_DEFAULT,
+                userId)
+                != PERMISSION_GRANTED) {
             return;
         }
 
         // convert up-time to current time.
-        final long installTimestamp = System.currentTimeMillis()
-                - (SystemClock.uptimeMillis() - appInfo.createTimestamp);
+        final long installTimestamp =
+                System.currentTimeMillis() - (SystemClock.uptimeMillis() - appInfo.createTimestamp);
 
         if (installedByAdb(initiatingPackageName)
                 || wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
@@ -257,8 +283,8 @@
         return PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName);
     }
 
-    private boolean wasForegroundInstallation(String installerPackageName,
-            int userId, long installTimestamp) {
+    private boolean wasForegroundInstallation(
+            String installerPackageName, int userId, long installTimestamp) {
         TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
                 mInstallerForegroundTimeFrames.get(userId, installerPackageName);
 
@@ -347,12 +373,12 @@
             for (int i = 0; i < mBackgroundInstalledPackages.size(); i++) {
                 int userId = mBackgroundInstalledPackages.keyAt(i);
                 for (String packageName : mBackgroundInstalledPackages.get(userId)) {
-                    long token = protoOutputStream.start(
-                            BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                    long token =
+                            protoOutputStream.start(
+                                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                     protoOutputStream.write(
                             BackgroundInstalledPackageProto.PACKAGE_NAME, packageName);
-                    protoOutputStream.write(
-                            BackgroundInstalledPackageProto.USER_ID, userId + 1);
+                    protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, userId + 1);
                     protoOutputStream.end(token);
                 }
             }
@@ -385,23 +411,28 @@
                         != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
                     continue;
                 }
-                long token = protoInputStream.start(
-                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                long token =
+                        protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                 String packageName = null;
                 int userId = UserHandle.USER_NULL;
                 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (protoInputStream.getFieldNumber()) {
                         case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
-                            packageName = protoInputStream.readString(
-                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            packageName =
+                                    protoInputStream.readString(
+                                            BackgroundInstalledPackageProto.PACKAGE_NAME);
                             break;
                         case (int) BackgroundInstalledPackageProto.USER_ID:
-                            userId = protoInputStream.readInt(
-                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            userId =
+                                    protoInputStream.readInt(
+                                            BackgroundInstalledPackageProto.USER_ID)
+                                            - 1;
                             break;
                         default:
-                            Slog.w(TAG, "Undefined field in proto: "
-                                    + protoInputStream.getFieldNumber());
+                            Slog.w(
+                                    TAG,
+                                    "Undefined field in proto: "
+                                            + protoInputStream.getFieldNumber());
                     }
                 }
                 protoInputStream.end(token);
@@ -430,9 +461,12 @@
         if (mInstallerForegroundTimeFrames.contains(userId, pkgName)) {
             return true;
         }
-        return mPermissionManager.checkPermission(pkgName,
-                android.Manifest.permission.INSTALL_PACKAGES, Context.DEVICE_ID_DEFAULT,
-                userId) == PackageManager.PERMISSION_GRANTED;
+        return mPermissionManager.checkPermission(
+                pkgName,
+                android.Manifest.permission.INSTALL_PACKAGES,
+                Context.DEVICE_ID_DEFAULT,
+                userId)
+                == PERMISSION_GRANTED;
     }
 
     @Override
@@ -446,21 +480,22 @@
             publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
         }
 
-        mPackageManagerInternal.getPackageList(new PackageManagerInternal.PackageListObserver() {
-            @Override
-            public void onPackageAdded(String packageName, int uid) {
-                final int userId = UserHandle.getUserId(uid);
-                mHandler.obtainMessage(MSG_PACKAGE_ADDED,
-                        userId, 0, packageName).sendToTarget();
-            }
+        mPackageManagerInternal.getPackageList(
+                new PackageManagerInternal.PackageListObserver() {
+                    @Override
+                    public void onPackageAdded(String packageName, int uid) {
+                        final int userId = UserHandle.getUserId(uid);
+                        mHandler.obtainMessage(MSG_PACKAGE_ADDED, userId, 0, packageName)
+                                .sendToTarget();
+                    }
 
-            @Override
-            public void onPackageRemoved(String packageName, int uid) {
-                final int userId = UserHandle.getUserId(uid);
-                mHandler.obtainMessage(MSG_PACKAGE_REMOVED,
-                        userId, 0, packageName).sendToTarget();
-            }
-        });
+                    @Override
+                    public void onPackageRemoved(String packageName, int uid) {
+                        final int userId = UserHandle.getUserId(uid);
+                        mHandler.obtainMessage(MSG_PACKAGE_REMOVED, userId, 0, packageName)
+                                .sendToTarget();
+                    }
+                });
     }
 
     // The foreground time frame (ForegroundTimeFrame) represents the period
@@ -516,7 +551,7 @@
     }
 
     /**
-     * Dependency injector for {@link #BackgroundInstallControlService)}.
+     * Dependency injector for {@link BackgroundInstallControlService}.
      */
     interface Injector {
         Context getContext();
@@ -532,6 +567,7 @@
         Looper getLooper();
 
         File getDiskFile();
+
     }
 
     private static final class InjectorImpl implements Injector {
@@ -568,11 +604,11 @@
 
         @Override
         public Looper getLooper() {
-            ServiceThread serviceThread = new ServiceThread(TAG,
-                    android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
+            ServiceThread serviceThread =
+                    new ServiceThread(
+                            TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
             serviceThread.start();
             return serviceThread.getLooper();
-
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 4f9eab9..f311034 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2900,6 +2900,12 @@
                     // code is loaded by a new Activity before ApplicationInfo changes have
                     // propagated to all application threads.
                     mPm.scheduleDeferredNoKillPostDelete(args);
+                    if (Flags.improveInstallDontKill()) {
+                        synchronized (mPm.mInstallLock) {
+                            PackageManagerServiceUtils.linkSplitsToOldDirs(mPm.mInstaller,
+                                    packageName, pkgSetting.getPath(), pkgSetting.getOldPaths());
+                        }
+                    }
                 } else {
                     mRemovePackageHelper.cleanUpResources(packageName, args.getCodeFile(),
                             args.getInstructionSets());
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index ac826af..b5346a3 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -1680,9 +1680,8 @@
                             mContext,
                             /* requestCode */ 0,
                             intent,
-                            PendingIntent.FLAG_ONE_SHOT
-                                    | PendingIntent.FLAG_IMMUTABLE
-                                    | PendingIntent.FLAG_CANCEL_CURRENT,
+                            PendingIntent.FLAG_IMMUTABLE
+                                    | FLAG_UPDATE_CURRENT,
                             /* options */ null,
                             user);
             return pi == null ? null : pi.getIntentSender();
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 09a91ed..1a20c8d 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -31,6 +31,7 @@
 import static android.content.pm.PackageManager.DELETE_ARCHIVE;
 import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
 import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
+import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
 import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE;
 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
 
@@ -66,8 +67,11 @@
 import android.graphics.Color;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
 import android.graphics.drawable.LayerDrawable;
 import android.os.Binder;
 import android.os.Bundle;
@@ -379,9 +383,8 @@
         verifyNotSystemApp(ps.getFlags());
         verifyInstalled(ps, userId);
         String responsibleInstallerPackage = getResponsibleInstallerPackage(ps);
-        verifyInstaller(responsibleInstallerPackage, userId);
-        ApplicationInfo installerInfo = snapshot.getApplicationInfo(
-                responsibleInstallerPackage, /* flags= */ 0, userId);
+        ApplicationInfo installerInfo = verifyInstaller(
+                snapshot, responsibleInstallerPackage, userId);
         verifyOptOutStatus(packageName,
                 UserHandle.getUid(userId, UserHandle.getUid(userId, ps.getAppId())));
 
@@ -421,10 +424,10 @@
             List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size());
             for (int i = 0, size = mainActivities.size(); i < size; ++i) {
                 var mainActivity = mainActivities.get(i);
-                Path iconPath = storeDrawable(
-                        packageName, mainActivity.getIcon(), userId, i, iconSize);
-                Path monochromePath =  storeDrawable(
-                        packageName, mainActivity.getMonochromeIcon(), userId, i, iconSize);
+                Path iconPath = storeAdaptiveDrawable(
+                        packageName, mainActivity.getIcon(), userId, i * 2 + 0, iconSize);
+                Path monochromePath = storeAdaptiveDrawable(
+                        packageName, mainActivity.getMonochromeIcon(), userId, i * 2 + 1, iconSize);
                 ArchiveActivityInfo activityInfo =
                         new ArchiveActivityInfo(
                                 mainActivity.getLabel().toString(),
@@ -451,7 +454,8 @@
         List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size());
         for (int i = 0, size = mainActivities.size(); i < size; i++) {
             LauncherActivityInfo mainActivity = mainActivities.get(i);
-            Path iconPath = storeIcon(packageName, mainActivity, userId, i, iconSize);
+            Path iconPath = storeIcon(packageName, mainActivity, userId, i * 2 + 0, iconSize);
+            // i * 2 + 1 reserved for monochromeIcon
             ArchiveActivityInfo activityInfo =
                     new ArchiveActivityInfo(
                             mainActivity.getLabel().toString(),
@@ -495,8 +499,30 @@
         return iconFile.toPath();
     }
 
-    private void verifyInstaller(String installerPackageName, int userId)
-            throws PackageManager.NameNotFoundException {
+    /**
+     * Create an <a
+     * href="https://developer.android.com/develop/ui/views/launch/icon_design_adaptive">
+     * adaptive icon</a> from an icon.
+     * This is necessary so the icon can be displayed properly by different launchers.
+     */
+    private static Path storeAdaptiveDrawable(String packageName, @Nullable Drawable iconDrawable,
+            @UserIdInt int userId, int index, int iconSize) throws IOException {
+        if (iconDrawable == null) {
+            return null;
+        }
+
+        // see BaseIconFactory#createShapedIconBitmap
+        float inset = getExtraInsetFraction();
+        inset = inset / (1 + 2 * inset);
+        Drawable d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK),
+                new InsetDrawable(iconDrawable/*d*/, inset, inset, inset, inset));
+
+        return storeDrawable(packageName, d, userId, index, iconSize);
+    }
+
+
+    private ApplicationInfo verifyInstaller(Computer snapshot, String installerPackageName,
+            int userId) throws PackageManager.NameNotFoundException {
         if (TextUtils.isEmpty(installerPackageName)) {
             throw new PackageManager.NameNotFoundException("No installer found");
         }
@@ -505,6 +531,12 @@
                 && !verifySupportsUnarchival(installerPackageName, userId)) {
             throw new PackageManager.NameNotFoundException("Installer does not support unarchival");
         }
+        ApplicationInfo appInfo = snapshot.getApplicationInfo(
+                installerPackageName, /* flags=*/ 0, userId);
+        if (appInfo == null) {
+            throw new PackageManager.NameNotFoundException("Failed to obtain Installer info");
+        }
+        return appInfo;
     }
 
     /**
@@ -570,7 +602,7 @@
         }
 
         try {
-            verifyInstaller(getResponsibleInstallerPackage(ps), userId);
+            verifyInstaller(snapshot, getResponsibleInstallerPackage(ps), userId);
             getLauncherActivityInfos(packageName, userId);
         } catch (PackageManager.NameNotFoundException e) {
             return false;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 7bf9fe7..cfafe7c 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -788,6 +788,24 @@
             }
         }
 
+        if (Flags.recoverabilityDetection()) {
+            if (params.rollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH
+                    || params.rollbackImpactLevel
+                    == PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL) {
+                if ((params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
+                    throw new IllegalArgumentException(
+                            "Can't set rollbackImpactLevel when rollback is not enabled");
+                }
+                if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ROLLBACKS)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException(
+                            "Setting rollbackImpactLevel requires the MANAGE_ROLLBACKS permission");
+                }
+            } else if (params.rollbackImpactLevel < 0) {
+                throw new IllegalArgumentException("rollbackImpactLevel can't be negative.");
+            }
+        }
+
         boolean isApex = (params.installFlags & PackageManager.INSTALL_APEX) != 0;
         if (isApex) {
             if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGE_UPDATES)
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 117d03f..27c3dad 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -54,6 +54,7 @@
 import static com.android.internal.util.XmlUtils.writeUriAttribute;
 import static com.android.server.pm.PackageInstallerService.prepareStageDir;
 import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME;
+import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE;
 import static com.android.server.pm.PackageManagerServiceUtils.isInstalledByAdb;
 import static com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata;
 
@@ -1294,6 +1295,7 @@
             info.autoRevokePermissionsMode = params.autoRevokePermissionsMode;
             info.installFlags = params.installFlags;
             info.rollbackLifetimeMillis = params.rollbackLifetimeMillis;
+            info.rollbackImpactLevel = params.rollbackImpactLevel;
             info.isMultiPackage = params.isMultiPackage;
             info.isStaged = params.isStaged;
             info.rollbackDataPolicy = params.rollbackDataPolicy;
@@ -1831,7 +1833,7 @@
             try {
                 Os.link(path, sourcePath);
                 // Grant READ access for APK to be read successfully
-                Os.chmod(sourcePath, 0644);
+                Os.chmod(sourcePath, DEFAULT_FILE_ACCESS_MODE);
             } catch (ErrnoException e) {
                 e.rethrowAsIOException();
             }
@@ -1900,7 +1902,8 @@
 
             // If file is app metadata then set permission to 0640 to deny user read access since it
             // might contain sensitive information.
-            int mode = name.equals(APP_METADATA_FILE_NAME) ? APP_METADATA_FILE_ACCESS_MODE : 0644;
+            int mode = name.equals(APP_METADATA_FILE_NAME)
+                    ? APP_METADATA_FILE_ACCESS_MODE : DEFAULT_FILE_ACCESS_MODE;
             ParcelFileDescriptor targetPfd = openTargetInternal(target.getAbsolutePath(),
                     O_CREAT | O_WRONLY, mode);
             Os.chmod(target.getAbsolutePath(), mode);
@@ -4245,7 +4248,7 @@
                 throw new IOException("Failed to copy " + fromFile + " to " + tmpFile);
             }
             try {
-                Os.chmod(tmpFile.getAbsolutePath(), 0644);
+                Os.chmod(tmpFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
             } catch (ErrnoException e) {
                 throw new IOException("Failed to chmod " + tmpFile);
             }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index c5b5a76..609a703 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -593,6 +593,8 @@
 
     static final String APP_METADATA_FILE_NAME = "app.metadata";
 
+    static final int DEFAULT_FILE_ACCESS_MODE = 0644;
+
     final Handler mHandler;
     final Handler mBackgroundHandler;
 
@@ -4608,6 +4610,7 @@
             });
             // Send UNSTOPPED broadcast if necessary
             if (wasStopped && Flags.stayStopped()) {
+                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "unstoppedBroadcast");
                 final PackageManagerInternal pmi =
                         mInjector.getLocalService(PackageManagerInternal.class);
                 final int [] userIds = resolveUserIds(userId);
@@ -4627,6 +4630,7 @@
                 mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_UNSTOPPED,
                         packageName, extras, userIds, null /* instantUserIds */,
                         broadcastAllowList, mHandler, null /* filterExtras */);
+                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
             }
         }
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index cd34163..8531692 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -30,6 +30,7 @@
 import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION;
 import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING;
 import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED;
+import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE;
 import static com.android.server.pm.PackageManagerService.RANDOM_CODEPATH_PREFIX;
 import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX;
 import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME;
@@ -69,6 +70,7 @@
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Process;
+import android.os.SELinux;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.incremental.IncrementalManager;
@@ -129,10 +131,12 @@
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.zip.GZIPInputStream;
@@ -853,7 +857,7 @@
             FileUtils.copy(fileIn, outputStream);
             // Flush anything in buffer before chmod, because any writes after chmod will fail.
             outputStream.flush();
-            Os.fchmod(outputStream.getFD(), 0644);
+            Os.fchmod(outputStream.getFD(), DEFAULT_FILE_ACCESS_MODE);
             atomicFile.finishWrite(outputStream);
             return PackageManager.INSTALL_SUCCEEDED;
         } catch (IOException e) {
@@ -1081,8 +1085,8 @@
 
         final File targetFile = new File(targetDir, targetName);
         final FileDescriptor targetFd = Os.open(targetFile.getAbsolutePath(),
-                O_RDWR | O_CREAT, 0644);
-        Os.chmod(targetFile.getAbsolutePath(), 0644);
+                O_RDWR | O_CREAT, DEFAULT_FILE_ACCESS_MODE);
+        Os.chmod(targetFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
         FileInputStream source = null;
         try {
             source = new FileInputStream(sourcePath);
@@ -1552,4 +1556,72 @@
     public static boolean isInstalledByAdb(String initiatingPackageName) {
         return initiatingPackageName == null || SHELL_PACKAGE_NAME.equals(initiatingPackageName);
     }
+
+    public static void linkSplitsToOldDirs(@NonNull Installer installer,
+                                           @NonNull String packageName,
+                                           @NonNull File newPath,
+                                           @Nullable Set<File> oldPaths) {
+        if (oldPaths == null || oldPaths.isEmpty()) {
+            return;
+        }
+        if (IncrementalManager.isIncrementalPath(newPath.getPath())) {
+            //TODO(b/291212866): handle incremental installs
+            return;
+        }
+        final File[] filesInNewPath = newPath.listFiles();
+        if (filesInNewPath == null || filesInNewPath.length == 0) {
+            return;
+        }
+        final List<String> splitApkNames = new ArrayList<String>();
+        for (int i = 0; i < filesInNewPath.length; i++) {
+            if (!filesInNewPath[i].isDirectory() && filesInNewPath[i].toString().endsWith(".apk")) {
+                splitApkNames.add(filesInNewPath[i].getName());
+            }
+        }
+        final int numSplits = splitApkNames.size();
+        if (numSplits == 0) {
+            return;
+        }
+        for (File oldPath : oldPaths) {
+            if (!oldPath.exists()) {
+                continue;
+            }
+            for (int i = 0; i < numSplits; i++) {
+                final String splitApkName = splitApkNames.get(i);
+                final File linkedSplit = new File(oldPath, splitApkName);
+                if (linkedSplit.exists()) {
+                    if (DEBUG) {
+                        Slog.d(PackageManagerService.TAG, "Skipping existing linked split <"
+                                + linkedSplit + ">");
+                    }
+                    continue;
+                }
+                final File sourceSplit = new File(newPath, splitApkName);
+                try {
+                    installer.linkFile(packageName, splitApkName,
+                            newPath.getAbsolutePath(), oldPath.getAbsolutePath());
+                    if (DEBUG) {
+                        Slog.d(PackageManagerService.TAG, "Linked <"
+                                + sourceSplit + "> to <" + linkedSplit + ">");
+                    }
+                } catch (Installer.InstallerException e) {
+                    Slog.w(PackageManagerService.TAG, "Failed to link split <"
+                            + sourceSplit + " > to <" + linkedSplit + ">", e);
+                    continue;
+                }
+                try {
+                    Os.chmod(linkedSplit.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
+                } catch (ErrnoException e) {
+                    Slog.w(PackageManagerService.TAG, "Failed to set mode for linked split <"
+                            + linkedSplit + ">", e);
+                    continue;
+                }
+                if (!SELinux.restorecon(linkedSplit)) {
+                    Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked split <"
+                            + linkedSplit + ">");
+                }
+            }
+        }
+        //TODO(b/291212866): support native libs
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index ca00c84..5c9c8c6 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -28,6 +28,7 @@
 import static android.content.pm.PackageManager.RESTRICTION_NONE;
 
 import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
+import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 
 import android.accounts.IAccountManager;
@@ -297,6 +298,8 @@
                     return runSetHiddenSetting(true);
                 case "unhide":
                     return runSetHiddenSetting(false);
+                case "unstop":
+                    return runSetStoppedState(false);
                 case "suspend":
                     return runSuspend(true, 0);
                 case "suspend-quarantine":
@@ -2347,7 +2350,7 @@
                 Streams.copy(inStream, outStream);
             }
             // Give read permissions to the other group.
-            Os.chmod(outputProfilePath, /*mode*/ 0644 );
+            Os.chmod(outputProfilePath, /*mode*/ DEFAULT_FILE_ACCESS_MODE);
         } catch (IOException | ErrnoException e) {
             pw.println("Error when reading the profile fd: " + e.getMessage());
             e.printStackTrace(pw);
@@ -2662,6 +2665,26 @@
         return 0;
     }
 
+    private int runSetStoppedState(boolean state) throws RemoteException {
+        int userId = UserHandle.USER_SYSTEM;
+        String option = getNextOption();
+        if (option != null && option.equals("--user")) {
+            userId = UserHandle.parseUserArg(getNextArgRequired());
+        }
+
+        String pkg = getNextArg();
+        if (pkg == null) {
+            getErrPrintWriter().println("Error: no package specified");
+            return 1;
+        }
+        final int translatedUserId =
+                translateUserId(userId, UserHandle.USER_NULL, "runSetStoppedState");
+        mInterface.setPackageStoppedState(pkg, state, translatedUserId);
+        getOutPrintWriter().println("Package " + pkg + " new stopped state: "
+                + mInterface.isPackageStoppedForUser(pkg, translatedUserId));
+        return 0;
+    }
+
     private int runSetDistractingRestriction() {
         final PrintWriter pw = getOutPrintWriter();
         int userId = UserHandle.USER_SYSTEM;
@@ -3694,7 +3717,19 @@
                         // remember to set it themselves.
                         params.installerPackageName = "com.android.shell";
                     }
-                    sessionParams.installFlags |= PackageManager.INSTALL_ENABLE_ROLLBACK;
+                    int rollbackStrategy = PackageManager.ROLLBACK_DATA_POLICY_RESTORE;
+                    try {
+                        rollbackStrategy = Integer.parseInt(peekNextArg());
+                        if (rollbackStrategy < PackageManager.ROLLBACK_DATA_POLICY_RESTORE
+                                || rollbackStrategy > PackageManager.ROLLBACK_DATA_POLICY_RETAIN) {
+                            throw new IllegalArgumentException(
+                                    rollbackStrategy + " is not a valid rollback data policy.");
+                        }
+                        getNextArg(); // pop the argument
+                    } catch (NumberFormatException e) {
+                        // not followed by a number assume ROLLBACK_DATA_POLICY_RESTORE.
+                    }
+                    sessionParams.setEnableRollback(true, rollbackStrategy);
                     break;
                 case "--staged-ready-timeout":
                     params.stagedReadyTimeoutMs = Long.parseLong(getNextArgRequired());
@@ -3729,6 +3764,11 @@
         } else if (staged) {
             sessionParams.setStaged();
         }
+        if ((sessionParams.installFlags & PackageManager.INSTALL_APEX) != 0
+                && (sessionParams.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0
+                && sessionParams.rollbackDataPolicy == PackageManager.ROLLBACK_DATA_POLICY_WIPE) {
+            throw new IllegalArgumentException("Data policy 'wipe' is not supported for apex.");
+        }
         return params;
     }
 
@@ -4807,7 +4847,7 @@
         pw.println("       [--install-reason 0/1/2/3/4] [--originating-uri URI]");
         pw.println("       [--referrer URI] [--abi ABI_NAME] [--force-sdk]");
         pw.println("       [--preload] [--instant] [--full] [--dont-kill]");
-        pw.println("       [--enable-rollback]");
+        pw.println("       [--enable-rollback [0/1/2]]");
         pw.println("       [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]");
         pw.println("       [--apex] [--non-staged] [--force-non-staged]");
         pw.println("       [--staged-ready-timeout TIMEOUT] [--ignore-dexopt-profile]");
@@ -4831,6 +4871,8 @@
         pw.println("      --abi: override the default ABI of the platform");
         pw.println("      --instant: cause the app to be installed as an ephemeral install app");
         pw.println("      --full: cause the app to be installed as a non-ephemeral full app");
+        pw.println("      --enable-rollback: enable rollbacks for the upgrade.");
+        pw.println("          0=restore (default), 1=wipe, 2=retain");
         pw.println("      --install-location: force the install location:");
         pw.println("          0=auto, 1=internal only, 2=prefer external");
         pw.println("      --install-reason: indicates why the app is being installed:");
@@ -4934,6 +4976,8 @@
         pw.println("  hide [--user USER_ID] PACKAGE_OR_COMPONENT");
         pw.println("  unhide [--user USER_ID] PACKAGE_OR_COMPONENT");
         pw.println("");
+        pw.println("  unstop [--user USER_ID] PACKAGE");
+        pw.println("");
         pw.println("  suspend [--user USER_ID] PACKAGE [PACKAGE...]");
         pw.println("    Suspends the specified package(s) (as user).");
         pw.println("");
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c94111c..a6598d6 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -712,19 +712,24 @@
             boolean isAutoLockOnDeviceLockSelected =
                     autoLockPreference == Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK;
             if (isKeyguardLocked && isAutoLockOnDeviceLockSelected) {
-                int privateProfileUserId = getPrivateProfileUserId();
-                if (privateProfileUserId != UserHandle.USER_NULL) {
-                    Slog.i(LOG_TAG, "Auto-locking private space with user-id "
-                            + privateProfileUserId);
-                    setQuietModeEnabledAsync(privateProfileUserId,
-                            /* enableQuietMode */true, /* target */ null,
-                            mContext.getPackageName());
-                }
+                autoLockPrivateSpace();
             }
         }
     }
 
     @VisibleForTesting
+    void autoLockPrivateSpace() {
+        int privateProfileUserId = getPrivateProfileUserId();
+        if (privateProfileUserId != UserHandle.USER_NULL) {
+            Slog.i(LOG_TAG, "Auto-locking private space with user-id "
+                    + privateProfileUserId);
+            setQuietModeEnabledAsync(privateProfileUserId,
+                    /* enableQuietMode */true, /* target */ null,
+                    mContext.getPackageName());
+        }
+    }
+
+    @VisibleForTesting
     void setQuietModeEnabledAsync(@UserIdInt int userId, boolean enableQuietMode,
             IntentSender target, @Nullable String callingPackage) {
         if (android.multiuser.Flags.moveQuietModeOperationsToSeparateThread()) {
@@ -1036,9 +1041,18 @@
             }
         }
 
+        if (isAutoLockingPrivateSpaceOnRestartsEnabled()) {
+            autoLockPrivateSpace();
+        }
+
         markEphemeralUsersForRemoval();
     }
 
+    private boolean isAutoLockingPrivateSpaceOnRestartsEnabled() {
+        return android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enablePrivateSpaceAutolockOnRestarts();
+    }
+
     /**
      * This method retrieves the  {@link UserManagerInternal} only for the purpose of
      * PackageManagerService construction.
diff --git a/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java b/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java
index 3efac81..d138606 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java
@@ -26,6 +26,7 @@
 public final class PermissionAllowlist {
     @NonNull
     private final ArrayMap<String, ArrayMap<String, Boolean>> mOemAppAllowlist = new ArrayMap<>();
+
     @NonNull
     private final ArrayMap<String, ArrayMap<String, Boolean>> mPrivilegedAppAllowlist =
             new ArrayMap<>();
@@ -43,6 +44,19 @@
             mApexPrivilegedAppAllowlists = new ArrayMap<>();
 
     @NonNull
+    private final ArrayMap<String, ArrayMap<String, Boolean>> mSignatureAppAllowlist =
+            new ArrayMap<>();
+    @NonNull
+    private final ArrayMap<String, ArrayMap<String, Boolean>> mVendorSignatureAppAllowlist =
+            new ArrayMap<>();
+    @NonNull
+    private final ArrayMap<String, ArrayMap<String, Boolean>> mProductSignatureAppAllowlist =
+            new ArrayMap<>();
+    @NonNull
+    private final ArrayMap<String, ArrayMap<String, Boolean>> mSystemExtSignatureAppAllowlist =
+            new ArrayMap<>();
+
+    @NonNull
     public ArrayMap<String, ArrayMap<String, Boolean>> getOemAppAllowlist() {
         return mOemAppAllowlist;
     }
@@ -73,6 +87,26 @@
         return mApexPrivilegedAppAllowlists;
     }
 
+    @NonNull
+    public ArrayMap<String, ArrayMap<String, Boolean>> getSignatureAppAllowlist() {
+        return mSignatureAppAllowlist;
+    }
+
+    @NonNull
+    public ArrayMap<String, ArrayMap<String, Boolean>> getVendorSignatureAppAllowlist() {
+        return mVendorSignatureAppAllowlist;
+    }
+
+    @NonNull
+    public ArrayMap<String, ArrayMap<String, Boolean>> getProductSignatureAppAllowlist() {
+        return mProductSignatureAppAllowlist;
+    }
+
+    @NonNull
+    public ArrayMap<String, ArrayMap<String, Boolean>> getSystemExtSignatureAppAllowlist() {
+        return mSystemExtSignatureAppAllowlist;
+    }
+
     @Nullable
     public Boolean getOemAppAllowlistState(@NonNull String packageName,
             @NonNull String permissionName) {
@@ -137,4 +171,44 @@
         }
         return permissions.get(permissionName);
     }
+
+    @Nullable
+    public Boolean getSignatureAppAllowlistState(@NonNull String packageName,
+            @NonNull String permissionName) {
+        ArrayMap<String, Boolean> permissions = mSignatureAppAllowlist.get(packageName);
+        if (permissions == null) {
+            return null;
+        }
+        return permissions.get(permissionName);
+    }
+
+    @Nullable
+    public Boolean getVendorSignatureAppAllowlistState(@NonNull String packageName,
+            @NonNull String permissionName) {
+        ArrayMap<String, Boolean> permissions = mVendorSignatureAppAllowlist.get(packageName);
+        if (permissions == null) {
+            return null;
+        }
+        return permissions.get(permissionName);
+    }
+
+    @Nullable
+    public Boolean getProductSignatureAppAllowlistState(@NonNull String packageName,
+            @NonNull String permissionName) {
+        ArrayMap<String, Boolean> permissions = mProductSignatureAppAllowlist.get(packageName);
+        if (permissions == null) {
+            return null;
+        }
+        return permissions.get(permissionName);
+    }
+
+    @Nullable
+    public Boolean getSystemExtSignatureAppAllowlistState(@NonNull String packageName,
+            @NonNull String permissionName) {
+        ArrayMap<String, Boolean> permissions = mSystemExtSignatureAppAllowlist.get(packageName);
+        if (permissions == null) {
+            return null;
+        }
+        return permissions.get(permissionName);
+    }
 }
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
index c737283..f7603b5 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
@@ -27,6 +27,8 @@
 import com.android.server.pm.PackageKeySetData;
 import com.android.server.pm.permission.LegacyPermissionState;
 
+import java.io.File;
+import java.util.Set;
 import java.util.UUID;
 
 /**
@@ -111,4 +113,7 @@
      */
     @Nullable
     String getAppMetadataFilePath();
+
+    @Nullable
+    Set<File> getOldPaths();
 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index bf669fb..0abf304 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -5634,7 +5634,7 @@
             Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn",
                     0 /* cookie */);
             updateScreenOffSleepToken(false /* acquire */, false /* isSwappingDisplay */);
-            mDefaultDisplayPolicy.screenTurnedOn(screenOnListener);
+            mDefaultDisplayPolicy.screenTurningOn(screenOnListener);
             mBootAnimationDismissable = false;
 
             synchronized (mLock) {
@@ -5676,6 +5676,7 @@
                 mKeyguardDelegate.onScreenTurnedOn();
             }
         }
+        mDefaultDisplayPolicy.screenTurnedOn();
         reportScreenStateToVrManager(true);
     }
 
diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
index 5e8b4de..7808c4e 100644
--- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
+++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
@@ -30,6 +30,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.Signature;
 import android.os.Environment;
+import android.permission.flags.Flags;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -368,6 +369,7 @@
             dataOutputStream.writeUTF(profileOwner);
             dataOutputStream.writeInt(Settings.Global.getInt(mContext.getContentResolver(),
                     Settings.Global.DEVICE_DEMO_MODE, 0));
+            dataOutputStream.writeBoolean(Flags.walletRoleEnabled());
             dataOutputStream.flush();
         } catch (IOException e) {
             // Never happens for MessageDigestOutputStream and DataOutputStream.
diff --git a/services/core/java/com/android/server/policy/window_policy_flags.aconfig b/services/core/java/com/android/server/policy/window_policy_flags.aconfig
index ed981e0..2154a26 100644
--- a/services/core/java/com/android/server/policy/window_policy_flags.aconfig
+++ b/services/core/java/com/android/server/policy/window_policy_flags.aconfig
@@ -4,5 +4,5 @@
     name: "support_input_wakeup_delegate"
     namespace: "wear_frameworks"
     description: "Whether or not window policy allows injecting input wake-up delegate."
-    bug: "298055811"
+    bug: "319132073"
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index 871e98b..4bf8a78 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -319,6 +319,11 @@
                 pd.setMax(100);
                 pd.setProgress(0);
                 pd.setIndeterminate(false);
+                boolean showPercent = context.getResources().getBoolean(
+                    com.android.internal.R.bool.config_showPercentageTextDuringRebootToUpdate);
+                if (!showPercent) {
+                    pd.setProgressPercentFormat(null);
+                }
                 pd.setProgressNumberFormat(null);
                 pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
                 pd.setMessage(context.getText(
@@ -911,4 +916,4 @@
                     com.android.internal.R.string.config_defaultShutdownVibrationFile);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/rollback/README.md b/services/core/java/com/android/server/rollback/README.md
index 08800da..f6bcbd0 100644
--- a/services/core/java/com/android/server/rollback/README.md
+++ b/services/core/java/com/android/server/rollback/README.md
@@ -187,13 +187,22 @@
 
 ### Installing an App with Rollback Enabled
 
-The `adb install` command accepts the `--enable-rollback` flag to install an app
+The `adb install` command accepts the `--enable-rollback [0/1/2]` flag to install an app
 with rollback enabled. For example:
 
 ```
 $ adb install --enable-rollback FooV2.apk
 ```
 
+The default rollback data policy is `ROLLBACK_DATA_POLICY_RESTORE` (0). To use
+a different `RollbackDataPolicy`, like `ROLLBACK_DATA_POLICY_RETAIN` (1) or
+`ROLLBACK_DATA_POLICY_WIPE` (2), provide the int value after
+`--enable-rollback`. For example:
+
+```
+$ adb install --enable-rollback 1 FooV2.apk
+```
+
 ### Triggering Rollback Manually
 
 If rollback is available for an application, the pm command can be used to
@@ -217,7 +226,7 @@
   -state: committed
   -timestamp: 2019-04-23T14:57:35.944Z
   -packages:
-    com.android.tests.rollback.testapp.B 2 -> 1
+    com.android.tests.rollback.testapp.B 2 -> 1 [0]
   -causePackages:
   -committedSessionId: 1845805640
 649899517:
@@ -225,7 +234,7 @@
   -timestamp: 2019-04-23T12:55:21.342Z
   -stagedSessionId: 343374391
   -packages:
-    com.android.tests.rollback.testapex 2 -> 1
+    com.android.tests.rollback.testapex 2 -> 1 [0]
   -causePackages:
   -committedSessionId: 2096717281
 ```
@@ -233,7 +242,8 @@
 The example above shows two recently committed rollbacks. The update of
 com.android.tests.rollback.testapp.B from version 1 to version 2 was rolled
 back, and the update of com.android.tests.rollback.testapex from version 1 to
-version 2 was rolled back.
+version 2 was rolled back. For each package the value inside '[' and ']'
+indicates the `RollbackDataPolicy` for the rollback back.
 
 The state is 'available' or 'committed'. The timestamp gives the time when the
 rollback was first made available. If a stagedSessionId is present, then the
diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java
index a5b90f1..d1f91c8 100644
--- a/services/core/java/com/android/server/rollback/Rollback.java
+++ b/services/core/java/com/android/server/rollback/Rollback.java
@@ -215,7 +215,8 @@
                 /* packages */ new ArrayList<>(),
                 /* isStaged */ isStaged,
                 /* causePackages */ new ArrayList<>(),
-                /* committedSessionId */ -1);
+                /* committedSessionId */ -1,
+                /* rollbackImpactLevel */ PackageManager.ROLLBACK_USER_IMPACT_LOW);
         mUserId = userId;
         mInstallerPackageName = installerPackageName;
         mBackupDir = backupDir;
@@ -394,7 +395,8 @@
      */
     @WorkerThread
     boolean enableForPackage(String packageName, long newVersion, long installedVersion,
-            boolean isApex, String sourceDir, String[] splitSourceDirs, int rollbackDataPolicy) {
+            boolean isApex, String sourceDir, String[] splitSourceDirs, int rollbackDataPolicy,
+            @PackageManager.RollbackImpactLevel int rollbackImpactLevel) {
         assertInWorkerThread();
         try {
             RollbackStore.backupPackageCodePath(this, packageName, sourceDir);
@@ -415,6 +417,10 @@
                 isApex, false /* isApkInApex */, new ArrayList<>(), rollbackDataPolicy);
 
         info.getPackages().add(packageRollbackInfo);
+
+        if (info.getRollbackImpactLevel() < rollbackImpactLevel) {
+            info.setRollbackImpactLevel(rollbackImpactLevel);
+        }
         return true;
     }
 
@@ -961,7 +967,8 @@
         for (PackageRollbackInfo pkg : info.getPackages()) {
             ipw.println(pkg.getPackageName()
                     + " " + pkg.getVersionRolledBackFrom().getLongVersionCode()
-                    + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode());
+                    + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode()
+                    + " [" + pkg.getRollbackDataPolicy() + "]");
         }
         ipw.decreaseIndent();
         if (isCommitted()) {
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 13f1141..359678b 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -954,7 +954,7 @@
         ApplicationInfo appInfo = pkgInfo.applicationInfo;
         return rollback.enableForPackage(packageName, newPackage.getVersionCode(),
                 pkgInfo.getLongVersionCode(), isApex, appInfo.sourceDir,
-                appInfo.splitSourceDirs, rollbackDataPolicy);
+                appInfo.splitSourceDirs, rollbackDataPolicy, session.rollbackImpactLevel);
     }
 
     @ExtThread
diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java
index 0af137f..14539d5 100644
--- a/services/core/java/com/android/server/rollback/RollbackStore.java
+++ b/services/core/java/com/android/server/rollback/RollbackStore.java
@@ -193,16 +193,27 @@
         json.put("isStaged", rollback.isStaged());
         json.put("causePackages", versionedPackagesToJson(rollback.getCausePackages()));
         json.put("committedSessionId", rollback.getCommittedSessionId());
+        if (Flags.recoverabilityDetection()) {
+            json.put("rollbackImpactLevel", rollback.getRollbackImpactLevel());
+        }
         return json;
     }
 
     private static RollbackInfo rollbackInfoFromJson(JSONObject json) throws JSONException {
-        return new RollbackInfo(
+        RollbackInfo rollbackInfo = new RollbackInfo(
                 json.getInt("rollbackId"),
                 packageRollbackInfosFromJson(json.getJSONArray("packages")),
                 json.getBoolean("isStaged"),
                 versionedPackagesFromJson(json.getJSONArray("causePackages")),
                 json.getInt("committedSessionId"));
+
+        if (Flags.recoverabilityDetection()) {
+                // to make it backward compatible.
+            rollbackInfo.setRollbackImpactLevel(json.optInt("rollbackImpactLevel",
+                    PackageManager.ROLLBACK_USER_IMPACT_LOW));
+        }
+
+        return rollbackInfo;
     }
 
     /**
diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
index b384725..92b5764 100755
--- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java
+++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
@@ -309,6 +309,24 @@
         }
     }
 
+    public SparseArray<String> getHardwareInputIdMap() {
+        synchronized (mLock) {
+            return mHardwareInputIdMap.clone();
+        }
+    }
+
+    public SparseArray<String> getHdmiInputIdMap() {
+        synchronized (mLock) {
+            return mHdmiInputIdMap.clone();
+        }
+    }
+
+    public Map<String, TvInputInfo> getInputMap() {
+        synchronized (mLock) {
+            return Collections.unmodifiableMap(mInputMap);
+        }
+    }
+
     public Map<String, List<String>> getHdmiParentInputMap() {
         synchronized (mLock) {
             return Collections.unmodifiableMap(mHdmiParentInputMap);
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 73fc8e9..e434df7 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -135,6 +135,7 @@
     private static final int APP_TAG_SELF = TunedInfo.APP_TAG_SELF;
     private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS =
             "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS";
+    private static final long UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds
 
     // There are two different formats of DVB frontend devices. One is /dev/dvb%d.frontend%d,
     // another one is /dev/dvb/adapter%d/frontend%d. Followings are the patterns for selecting the
@@ -174,7 +175,7 @@
     @GuardedBy("mLock")
     private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>();
 
-    private final WatchLogHandler mWatchLogHandler;
+    private final MessageHandler mMessageHandler;
 
     private final ActivityManager mActivityManager;
 
@@ -187,8 +188,8 @@
         super(context);
 
         mContext = context;
-        mWatchLogHandler = new WatchLogHandler(mContext.getContentResolver(),
-                IoThread.get().getLooper());
+        mMessageHandler =
+                new MessageHandler(mContext.getContentResolver(), IoThread.get().getLooper());
         mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener());
 
         mActivityManager =
@@ -372,10 +373,10 @@
                     // service to populate the hardware list.
                     serviceState = new ServiceState(component, userId);
                     userState.serviceStateMap.put(component, serviceState);
+                    updateServiceConnectionLocked(component, userId);
                 } else {
                     inputList.addAll(serviceState.hardwareInputMap.values());
                 }
-                updateServiceConnectionLocked(component, userId);
             } else {
                 try {
                     TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build();
@@ -510,6 +511,7 @@
         }
     }
 
+    @GuardedBy("mLock")
     private void startProfileLocked(int userId) {
         mRunningProfiles.add(userId);
         buildTvInputListLocked(userId, null);
@@ -538,8 +540,10 @@
             mCurrentUserId = userId;
             buildTvInputListLocked(userId, null);
             buildTvContentRatingSystemListLocked(userId);
-            mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_SWITCH_CONTENT_RESOLVER,
-                    getContentResolverForUser(userId)).sendToTarget();
+            mMessageHandler
+                    .obtainMessage(MessageHandler.MSG_SWITCH_CONTENT_RESOLVER,
+                            getContentResolverForUser(userId))
+                    .sendToTarget();
         }
     }
 
@@ -593,7 +597,7 @@
                         Slog.e(TAG, "error in unregisterCallback", e);
                     }
                 }
-                mContext.unbindService(serviceState.connection);
+                unbindService(serviceState);
                 it.remove();
             }
         }
@@ -661,7 +665,7 @@
                             Slog.e(TAG, "error in unregisterCallback", e);
                         }
                     }
-                    mContext.unbindService(serviceState.connection);
+                    unbindService(serviceState);
                 }
             }
             userState.serviceStateMap.clear();
@@ -774,7 +778,8 @@
 
         boolean shouldBind;
         if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) {
-            shouldBind = !serviceState.sessionTokens.isEmpty() || serviceState.isHardware;
+            shouldBind = !serviceState.sessionTokens.isEmpty()
+                    || (serviceState.isHardware && serviceState.neverConnected);
         } else {
             // For a non-current user,
             // if sessionTokens is not empty, it contains recording sessions only
@@ -783,31 +788,14 @@
             shouldBind = !serviceState.sessionTokens.isEmpty();
         }
 
-        if (serviceState.service == null && shouldBind) {
-            // This means that the service is not yet connected but its state indicates that we
-            // have pending requests. Then, connect the service.
-            if (serviceState.bound) {
-                // We have already bound to the service so we don't try to bind again until after we
-                // unbind later on.
-                return;
+        // only bind/unbind when necessary.
+        if (shouldBind && !serviceState.bound) {
+            bindService(serviceState, userId);
+        } else if (!shouldBind && serviceState.bound) {
+            unbindService(serviceState);
+            if (!serviceState.isHardware) {
+                userState.serviceStateMap.remove(component);
             }
-            if (DEBUG) {
-                Slog.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")");
-            }
-
-            Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component);
-            serviceState.bound = mContext.bindServiceAsUser(
-                    i, serviceState.connection,
-                    Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
-                    new UserHandle(userId));
-        } else if (serviceState.service != null && !shouldBind) {
-            // This means that the service is already connected but its state indicates that we have
-            // nothing to do with it. Then, disconnect the service.
-            if (DEBUG) {
-                Slog.d(TAG, "unbindService(service=" + component + ")");
-            }
-            mContext.unbindService(serviceState.connection);
-            userState.serviceStateMap.remove(component);
         }
     }
 
@@ -829,7 +817,11 @@
             sendSessionTokenToClientLocked(sessionState.client,
                     sessionState.inputId, null, null, sessionState.seq);
         }
-        updateServiceConnectionLocked(serviceState.component, userId);
+        if (!serviceState.isHardware) {
+            updateServiceConnectionLocked(serviceState.component, userId);
+        } else {
+            updateHardwareServiceConnectionDelayed(userId);
+        }
     }
 
     @GuardedBy("mLock")
@@ -948,13 +940,17 @@
         if (serviceState != null) {
             serviceState.sessionTokens.remove(sessionToken);
         }
-        updateServiceConnectionLocked(sessionState.componentName, userId);
+        if (!serviceState.isHardware) {
+            updateServiceConnectionLocked(sessionState.componentName, userId);
+        } else {
+            updateHardwareServiceConnectionDelayed(userId);
+        }
 
         // Log the end of watch.
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = sessionToken;
         args.arg2 = System.currentTimeMillis();
-        mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_END, args).sendToTarget();
+        mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_END, args).sendToTarget();
     }
 
     @GuardedBy("mLock")
@@ -1153,8 +1149,7 @@
         ServiceState serviceState = userState.serviceStateMap.get(inputState.info.getComponent());
         int oldState = inputState.state;
         inputState.state = state;
-        if (serviceState != null && serviceState.service == null
-                && (!serviceState.sessionTokens.isEmpty() || serviceState.isHardware)) {
+        if (serviceState != null && serviceState.reconnecting) {
             // We don't notify state change while reconnecting. It should remain disconnected.
             return;
         }
@@ -1881,7 +1876,7 @@
                         args.arg3 = ContentUris.parseId(channelUri);
                         args.arg4 = params;
                         args.arg5 = sessionToken;
-                        mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args)
+                        mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_START, args)
                                 .sendToTarget();
                     } catch (RemoteException | SessionNotFoundException e) {
                         Slog.e(TAG, "error in tune", e);
@@ -3327,16 +3322,21 @@
         private final ComponentName component;
         private final boolean isHardware;
         private final Map<String, TvInputInfo> hardwareInputMap = new HashMap<>();
+        private final List<TvInputHardwareInfo> hardwareDeviceRemovedBuffer = new ArrayList<>();
+        private final List<HdmiDeviceInfo> hdmiDeviceRemovedBuffer = new ArrayList<>();
+        private final List<HdmiDeviceInfo> hdmiDeviceUpdatedBuffer = new ArrayList<>();
 
         private ITvInputService service;
         private ServiceCallback callback;
         private boolean bound;
         private boolean reconnecting;
+        private boolean neverConnected;
 
         private ServiceState(ComponentName component, int userId) {
             this.component = component;
             this.connection = new InputServiceConnection(component, userId);
             this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component);
+            this.neverConnected = true;
         }
     }
 
@@ -3449,6 +3449,97 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private void bindService(ServiceState serviceState, int userId) {
+        if (serviceState.bound) {
+            // We have already bound to the service so we don't try to bind again until after we
+            // unbind later on.
+            // For hardware services, call updateHardwareServiceConnectionDelayed() to delay the
+            // possible unbinding.
+            if (serviceState.isHardware) {
+                updateHardwareServiceConnectionDelayed(userId);
+            }
+            return;
+        }
+        if (DEBUG) {
+            Slog.d(TAG,
+                    "bindServiceAsUser(service=" + serviceState.component + ", userId=" + userId
+                            + ")");
+        }
+        Intent i =
+                new Intent(TvInputService.SERVICE_INTERFACE).setComponent(serviceState.component);
+        serviceState.bound = mContext.bindServiceAsUser(i, serviceState.connection,
+                Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
+                new UserHandle(userId));
+        if (!serviceState.bound) {
+            Slog.e(TAG, "failed to bind " + serviceState.component + " for userId " + userId);
+            mContext.unbindService(serviceState.connection);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void unbindService(ServiceState serviceState) {
+        if (!serviceState.bound) {
+            return;
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "unbindService(service=" + serviceState.component + ")");
+        }
+        mContext.unbindService(serviceState.connection);
+        serviceState.bound = false;
+        serviceState.service = null;
+        serviceState.callback = null;
+    }
+
+    @GuardedBy("mLock")
+    private void updateHardwareTvInputServiceBindingLocked(int userId) {
+        PackageManager pm = mContext.getPackageManager();
+        List<ResolveInfo> services =
+                pm.queryIntentServicesAsUser(new Intent(TvInputService.SERVICE_INTERFACE),
+                        PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, userId);
+        for (ResolveInfo ri : services) {
+            ServiceInfo si = ri.serviceInfo;
+            if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) {
+                continue;
+            }
+            ComponentName component = new ComponentName(si.packageName, si.name);
+            if (hasHardwarePermission(pm, component)) {
+                updateServiceConnectionLocked(component, userId);
+            }
+        }
+    }
+
+    private void updateHardwareServiceConnectionDelayed(int userId) {
+        mMessageHandler.removeMessages(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING);
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = userId;
+        Message msg =
+                mMessageHandler.obtainMessage(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING, args);
+        mMessageHandler.sendMessageDelayed(msg, UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS);
+    }
+
+    @GuardedBy("mLock")
+    private void addHardwareInputLocked(
+            TvInputInfo inputInfo, ComponentName component, int userId) {
+        ServiceState serviceState = getServiceStateLocked(component, userId);
+        serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo);
+        buildTvInputListLocked(userId, null);
+    }
+
+    @GuardedBy("mLock")
+    private void removeHardwareInputLocked(String inputId, int userId) {
+        if (!mTvInputHardwareManager.getInputMap().containsKey(inputId)) {
+            return;
+        }
+        ComponentName component = mTvInputHardwareManager.getInputMap().get(inputId).getComponent();
+        ServiceState serviceState = getServiceStateLocked(component, userId);
+        boolean removed = serviceState.hardwareInputMap.remove(inputId) != null;
+        if (removed) {
+            buildTvInputListLocked(userId, null);
+            mTvInputHardwareManager.removeHardwareInput(inputId);
+        }
+    }
+
     private final class InputServiceConnection implements ServiceConnection {
         private final ComponentName mComponent;
         private final int mUserId;
@@ -3472,6 +3563,7 @@
                 }
                 ServiceState serviceState = userState.serviceStateMap.get(mComponent);
                 serviceState.service = ITvInputService.Stub.asInterface(service);
+                serviceState.neverConnected = false;
 
                 // Register a callback, if we need to.
                 if (serviceState.isHardware && serviceState.callback == null) {
@@ -3483,6 +3575,58 @@
                     }
                 }
 
+                for (TvInputState inputState : userState.inputMap.values()) {
+                    if (inputState.info.getComponent().equals(component)
+                            && inputState.state != INPUT_STATE_CONNECTED) {
+                        notifyInputStateChangedLocked(userState, inputState.info.getId(),
+                                inputState.state, null);
+                    }
+                }
+
+                if (serviceState.isHardware) {
+                    for (TvInputHardwareInfo hardwareToBeRemoved :
+                            serviceState.hardwareDeviceRemovedBuffer) {
+                        try {
+                            serviceState.service.notifyHardwareRemoved(hardwareToBeRemoved);
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "error in hardwareDeviceRemovedBuffer", e);
+                        }
+                    }
+                    serviceState.hardwareDeviceRemovedBuffer.clear();
+                    for (HdmiDeviceInfo hdmiDeviceToBeRemoved :
+                            serviceState.hdmiDeviceRemovedBuffer) {
+                        try {
+                            serviceState.service.notifyHdmiDeviceRemoved(hdmiDeviceToBeRemoved);
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "error in hdmiDeviceRemovedBuffer", e);
+                        }
+                    }
+                    serviceState.hdmiDeviceRemovedBuffer.clear();
+                    for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) {
+                        try {
+                            serviceState.service.notifyHardwareAdded(hardware);
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "error in notifyHardwareAdded", e);
+                        }
+                    }
+                    for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) {
+                        try {
+                            serviceState.service.notifyHdmiDeviceAdded(device);
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
+                        }
+                    }
+                    for (HdmiDeviceInfo hdmiDeviceToBeUpdated :
+                            serviceState.hdmiDeviceUpdatedBuffer) {
+                        try {
+                            serviceState.service.notifyHdmiDeviceUpdated(hdmiDeviceToBeUpdated);
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "error in hdmiDeviceUpdatedBuffer", e);
+                        }
+                    }
+                    serviceState.hdmiDeviceUpdatedBuffer.clear();
+                }
+
                 List<IBinder> tokensToBeRemoved = new ArrayList<>();
 
                 // And create sessions, if any.
@@ -3496,30 +3640,8 @@
                     removeSessionStateLocked(sessionToken, mUserId);
                 }
 
-                for (TvInputState inputState : userState.inputMap.values()) {
-                    if (inputState.info.getComponent().equals(component)
-                            && inputState.state != INPUT_STATE_CONNECTED) {
-                        notifyInputStateChangedLocked(userState, inputState.info.getId(),
-                                inputState.state, null);
-                    }
-                }
-
                 if (serviceState.isHardware) {
-                    serviceState.hardwareInputMap.clear();
-                    for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) {
-                        try {
-                            serviceState.service.notifyHardwareAdded(hardware);
-                        } catch (RemoteException e) {
-                            Slog.e(TAG, "error in notifyHardwareAdded", e);
-                        }
-                    }
-                    for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) {
-                        try {
-                            serviceState.service.notifyHdmiDeviceAdded(device);
-                        } catch (RemoteException e) {
-                            Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
-                        }
-                    }
+                    updateHardwareServiceConnectionDelayed(mUserId);
                 }
             }
         }
@@ -3570,13 +3692,6 @@
             }
         }
 
-        @GuardedBy("mLock")
-        private void addHardwareInputLocked(TvInputInfo inputInfo) {
-            ServiceState serviceState = getServiceStateLocked(mComponent, mUserId);
-            serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo);
-            buildTvInputListLocked(mUserId, null);
-        }
-
         public void addHardwareInput(int deviceId, TvInputInfo inputInfo) {
             ensureHardwarePermission();
             ensureValidInput(inputInfo);
@@ -3587,8 +3702,11 @@
                     if (serviceState.hardwareInputMap.containsKey(inputInfo.getId())) {
                         return;
                     }
+                    Slog.d("ServiceCallback",
+                            "addHardwareInput: device id " + deviceId + ", "
+                                    + inputInfo.toString());
                     mTvInputHardwareManager.addHardwareInput(deviceId, inputInfo);
-                    addHardwareInputLocked(inputInfo);
+                    addHardwareInputLocked(inputInfo, mComponent, mUserId);
                 }
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -3606,7 +3724,7 @@
                         return;
                     }
                     mTvInputHardwareManager.addHdmiInput(id, inputInfo);
-                    addHardwareInputLocked(inputInfo);
+                    addHardwareInputLocked(inputInfo, mComponent, mUserId);
                     if (mOnScreenInputId != null && mOnScreenSessionState != null) {
                         if (TextUtils.equals(mOnScreenInputId, inputInfo.getParentId())) {
                             // catch the use case when a CEC device is plugged in an HDMI port,
@@ -3635,14 +3753,9 @@
             final long identity = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    ServiceState serviceState = getServiceStateLocked(mComponent, mUserId);
-                    boolean removed = serviceState.hardwareInputMap.remove(inputId) != null;
-                    if (removed) {
-                        buildTvInputListLocked(mUserId, null);
-                        mTvInputHardwareManager.removeHardwareInput(inputId);
-                    } else {
-                        Slog.e(TAG, "failed to remove input " + inputId);
-                    }
+                    Slog.d("ServiceCallback",
+                            "removeHardwareInput " + inputId + " by " + mComponent);
+                    removeHardwareInputLocked(inputId, mUserId);
                 }
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -3860,6 +3973,23 @@
         }
 
         @Override
+        public void onVideoFreezeUpdated(boolean isFrozen) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "onVideoFreezeUpdated(" + isFrozen + ")");
+                }
+                if (mSessionState.session == null || mSessionState.client == null) {
+                    return;
+                }
+                try {
+                    mSessionState.client.onVideoFreezeUpdated(isFrozen, mSessionState.seq);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error in onVideoFreezeUpdated", e);
+                }
+            }
+        }
+
+        @Override
         public void onContentAllowed() {
             synchronized (mLock) {
                 if (DEBUG) {
@@ -4209,11 +4339,12 @@
         return loggedReason;
     }
 
+    @GuardedBy("mLock")
     private UserState getUserStateLocked(int userId) {
         return mUserStates.get(userId);
     }
 
-    private static final class WatchLogHandler extends Handler {
+    private final class MessageHandler extends Handler {
         // There are only two kinds of watch events that can happen on the system:
         // 1. The current TV input session is tuned to a new channel.
         // 2. The session is released for some reason.
@@ -4225,10 +4356,11 @@
         static final int MSG_LOG_WATCH_START = 1;
         static final int MSG_LOG_WATCH_END = 2;
         static final int MSG_SWITCH_CONTENT_RESOLVER = 3;
+        static final int MSG_UPDATE_HARDWARE_TIS_BINDING = 4;
 
         private ContentResolver mContentResolver;
 
-        WatchLogHandler(ContentResolver contentResolver, Looper looper) {
+        MessageHandler(ContentResolver contentResolver, Looper looper) {
             super(looper);
             mContentResolver = contentResolver;
         }
@@ -4287,6 +4419,14 @@
                     mContentResolver = (ContentResolver) msg.obj;
                     break;
                 }
+                case MSG_UPDATE_HARDWARE_TIS_BINDING:
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    int userId = (int) args.arg1;
+                    synchronized (mLock) {
+                        updateHardwareTvInputServiceBindingLocked(userId);
+                    }
+                    args.recycle();
+                    break;
                 default: {
                     Slog.w(TAG, "unhandled message code: " + msg.what);
                     break;
@@ -4342,29 +4482,46 @@
                 UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
                 // Broadcast the event to all hardware inputs.
                 for (ServiceState serviceState : userState.serviceStateMap.values()) {
-                    if (!serviceState.isHardware || serviceState.service == null) continue;
+                    if (!serviceState.isHardware) {
+                        continue;
+                    }
                     try {
-                        serviceState.service.notifyHardwareAdded(info);
+                        bindService(serviceState, mCurrentUserId);
+                        if (serviceState.service != null) {
+                            serviceState.service.notifyHardwareAdded(info);
+                        }
                     } catch (RemoteException e) {
                         Slog.e(TAG, "error in notifyHardwareAdded", e);
                     }
                 }
+                updateHardwareServiceConnectionDelayed(mCurrentUserId);
             }
         }
 
         @Override
         public void onHardwareDeviceRemoved(TvInputHardwareInfo info) {
             synchronized (mLock) {
+                String relatedInputId =
+                        mTvInputHardwareManager.getHardwareInputIdMap().get(info.getDeviceId());
+                removeHardwareInputLocked(relatedInputId, mCurrentUserId);
                 UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
                 // Broadcast the event to all hardware inputs.
                 for (ServiceState serviceState : userState.serviceStateMap.values()) {
-                    if (!serviceState.isHardware || serviceState.service == null) continue;
+                    if (!serviceState.isHardware) {
+                        continue;
+                    }
                     try {
-                        serviceState.service.notifyHardwareRemoved(info);
+                        bindService(serviceState, mCurrentUserId);
+                        if (serviceState.service != null) {
+                            serviceState.service.notifyHardwareRemoved(info);
+                        } else {
+                            serviceState.hardwareDeviceRemovedBuffer.add(info);
+                        }
                     } catch (RemoteException e) {
                         Slog.e(TAG, "error in notifyHardwareRemoved", e);
                     }
                 }
+                updateHardwareServiceConnectionDelayed(mCurrentUserId);
             }
         }
 
@@ -4374,29 +4531,46 @@
                 UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
                 // Broadcast the event to all hardware inputs.
                 for (ServiceState serviceState : userState.serviceStateMap.values()) {
-                    if (!serviceState.isHardware || serviceState.service == null) continue;
+                    if (!serviceState.isHardware) {
+                        continue;
+                    }
                     try {
-                        serviceState.service.notifyHdmiDeviceAdded(deviceInfo);
+                        bindService(serviceState, mCurrentUserId);
+                        if (serviceState.service != null) {
+                            serviceState.service.notifyHdmiDeviceAdded(deviceInfo);
+                        }
                     } catch (RemoteException e) {
                         Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
                     }
                 }
+                updateHardwareServiceConnectionDelayed(mCurrentUserId);
             }
         }
 
         @Override
         public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) {
             synchronized (mLock) {
+                String relatedInputId =
+                        mTvInputHardwareManager.getHdmiInputIdMap().get(deviceInfo.getId());
+                removeHardwareInputLocked(relatedInputId, mCurrentUserId);
                 UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
                 // Broadcast the event to all hardware inputs.
                 for (ServiceState serviceState : userState.serviceStateMap.values()) {
-                    if (!serviceState.isHardware || serviceState.service == null) continue;
+                    if (!serviceState.isHardware) {
+                        continue;
+                    }
                     try {
-                        serviceState.service.notifyHdmiDeviceRemoved(deviceInfo);
+                        bindService(serviceState, mCurrentUserId);
+                        if (serviceState.service != null) {
+                            serviceState.service.notifyHdmiDeviceRemoved(deviceInfo);
+                        } else {
+                            serviceState.hdmiDeviceRemovedBuffer.add(deviceInfo);
+                        }
                     } catch (RemoteException e) {
                         Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e);
                     }
                 }
+                updateHardwareServiceConnectionDelayed(mCurrentUserId);
             }
         }
 
@@ -4424,13 +4598,21 @@
                 UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
                 // Broadcast the event to all hardware inputs.
                 for (ServiceState serviceState : userState.serviceStateMap.values()) {
-                    if (!serviceState.isHardware || serviceState.service == null) continue;
+                    if (!serviceState.isHardware) {
+                        continue;
+                    }
                     try {
-                        serviceState.service.notifyHdmiDeviceUpdated(deviceInfo);
+                        bindService(serviceState, mCurrentUserId);
+                        if (serviceState.service != null) {
+                            serviceState.service.notifyHdmiDeviceUpdated(deviceInfo);
+                        } else {
+                            serviceState.hdmiDeviceUpdatedBuffer.add(deviceInfo);
+                        }
                     } catch (RemoteException e) {
                         Slog.e(TAG, "error in notifyHdmiDeviceUpdated", e);
                     }
                 }
+                updateHardwareServiceConnectionDelayed(mCurrentUserId);
             }
         }
 
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index 7ab075e..9c60fbb 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -1470,6 +1470,28 @@
         }
 
         @Override
+        public void notifyVideoFreezeUpdated(IBinder sessionToken, boolean isFrozen, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int callingPid = Binder.getCallingPid();
+            final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+                    "notifyVideoFreezeUpdated");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+                                resolvedUserId);
+                        getSessionLocked(sessionState).notifyVideoFreezeUpdated(isFrozen);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyVideoFreezeUpdated", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void notifyContentAllowed(IBinder sessionToken, int userId) {
             final int callingUid = Binder.getCallingUid();
             final int callingPid = Binder.getCallingPid();
@@ -1557,7 +1579,6 @@
             }
         }
 
-
         @Override
         public void notifyRecordingStarted(IBinder sessionToken, String recordingId,
                 String requestId, int userId) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 51acc8e..8549957 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -410,9 +410,10 @@
                 // adapt the entries in wallpaper.mCropHints for the actual display
                 SparseArray<Rect> updatedCropHints = new SparseArray<>();
                 for (int i = 0; i < wallpaper.mCropHints.size(); i++) {
-                    Rect defaultCrop = defaultDisplayCrops.valueAt(i);
+                    int orientation = wallpaper.mCropHints.keyAt(i);
+                    Rect defaultCrop = defaultDisplayCrops.get(orientation);
                     if (defaultCrop != null) {
-                        updatedCropHints.put(defaultDisplayCrops.keyAt(i), defaultCrop);
+                        updatedCropHints.put(orientation, defaultCrop);
                     }
                 }
                 wallpaper.mCropHints = updatedCropHints;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 9b1f9c8..036f7b6 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3965,20 +3965,6 @@
         return removedFromHistory;
     }
 
-    boolean safelyDestroy(String reason) {
-        if (isDestroyable()) {
-            if (DEBUG_SWITCH) {
-                final Task task = getTask();
-                Slog.v(TAG_SWITCH, "Safely destroying " + this + " in state " + getState()
-                        + " resumed=" + task.getTopResumedActivity()
-                        + " pausing=" + task.getTopPausingActivity()
-                        + " for reason " + reason);
-            }
-            return destroyImmediately(reason);
-        }
-        return false;
-    }
-
     /** Note: call {@link #cleanUp(boolean, boolean)} before this method. */
     void removeFromHistory(String reason) {
         finishActivityResults(Activity.RESULT_CANCELED,
@@ -4047,10 +4033,6 @@
         }
     }
 
-    boolean isFinishing() {
-        return finishing;
-    }
-
     /**
      * This method is to only be called from the client via binder when the activity is destroyed
      * AND finished.
@@ -7978,6 +7960,7 @@
         if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) {
             return;
         }
+        final int originalRelaunchingCount = mPendingRelaunchCount;
         // This is necessary in order to avoid going into size compat mode when the orientation
         // change request comes from the app
         if (getRequestedConfigurationOrientation(false, requestedOrientation)
@@ -7995,8 +7978,10 @@
         // the request is handled at task level with letterbox.
         if (!getMergedOverrideConfiguration().equals(
                 mLastReportedConfiguration.getMergedConfiguration())) {
-            ensureActivityConfiguration(
-                    false /* ignoreVisibility */, true /* isRequestedOrientationChanged */);
+            ensureActivityConfiguration(false /* ignoreVisibility */);
+            if (mPendingRelaunchCount > originalRelaunchingCount) {
+                mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true);
+            }
             if (mTransitionController.inPlayingTransition(this)) {
                 mTransitionController.mValidateActivityCompat.add(this);
             }
@@ -9502,11 +9487,6 @@
         return ensureActivityConfiguration(false /* ignoreVisibility */);
     }
 
-    boolean ensureActivityConfiguration(boolean ignoreVisibility) {
-        return ensureActivityConfiguration(ignoreVisibility,
-                false /* isRequestedOrientationChanged */);
-    }
-
     /**
      * Make sure the given activity matches the current configuration. Ensures the HistoryRecord
      * is updated with the correct configuration and all other bookkeeping is handled.
@@ -9515,13 +9495,10 @@
      *                         (stopped state). This is useful for the case where we know the
      *                         activity will be visible soon and we want to ensure its configuration
      *                         before we make it visible.
-     * @param isRequestedOrientationChanged whether this is triggered in response to an app calling
-     *                                      {@link android.app.Activity#setRequestedOrientation}.
      * @return False if the activity was relaunched and true if it wasn't relaunched because we
      *         can't or the app handles the specific configuration that is changing.
      */
-    boolean ensureActivityConfiguration(boolean ignoreVisibility,
-            boolean isRequestedOrientationChanged) {
+    boolean ensureActivityConfiguration(boolean ignoreVisibility) {
         final Task rootTask = getRootTask();
         if (rootTask.mConfigWillChange) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check "
@@ -9658,9 +9635,6 @@
             } else {
                 mRelaunchReason = RELAUNCH_REASON_NONE;
             }
-            if (isRequestedOrientationChanged) {
-                mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true);
-            }
             if (mState == PAUSING) {
                 // A little annoying: we are waiting for this activity to finish pausing. Let's not
                 // do anything now, but just flag that it needs to be restarted when done pausing.
diff --git a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java
index f1a2159..db27f60 100644
--- a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java
+++ b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java
@@ -43,7 +43,7 @@
     static final String DOC_LINK = "go/android-asm";
 
     /** Used to determine which version of the ASM logic was used in logs while we iterate */
-    static final int ASM_VERSION = 8;
+    static final int ASM_VERSION = 9;
 
     private static final String NAMESPACE = NAMESPACE_WINDOW_MANAGER;
     private static final String KEY_ASM_PREFIX = "ActivitySecurity__";
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index f6d77ea..d6f52b8 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2053,8 +2053,8 @@
         }
 
         if (!mSupervisor.getBackgroundActivityLaunchController().checkActivityAllowedToStart(
-                mSourceRecord, r, newTask, targetTask, mLaunchFlags, mBalCode, mCallingUid,
-                mRealCallingUid)) {
+                mSourceRecord, r, newTask, avoidMoveToFront(), targetTask, mLaunchFlags, mBalCode,
+                mCallingUid, mRealCallingUid)) {
             return START_ABORTED;
         }
 
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 2bd49bf..a4d15e0 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.View.FOCUS_FORWARD;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_NONE;
@@ -60,6 +61,7 @@
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.wm.utils.InsetUtils;
+import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -167,6 +169,24 @@
                 return null;
             }
 
+            // Move focus to the adjacent embedded window if it is higher than this window
+            final TaskFragment taskFragment = window.getTaskFragment();
+            final TaskFragment adjacentTaskFragment =
+                    taskFragment != null ? taskFragment.getAdjacentTaskFragment() : null;
+            if (adjacentTaskFragment != null && taskFragment.isEmbedded()
+                    && Flags.embeddedActivityBackNavFlag()) {
+                final WindowContainer parent = taskFragment.getParent();
+                if (parent.mChildren.indexOf(taskFragment) < parent.mChildren.indexOf(
+                        adjacentTaskFragment)) {
+                    mWindowManagerService.moveFocusToAdjacentWindow(window, FOCUS_FORWARD);
+                    window = wmService.getFocusedWindowLocked();
+                    if (window == null) {
+                        Slog.e(TAG, "Adjacent window is null, returning null.");
+                        return null;
+                    }
+                }
+            }
+
             // This is needed to bridge the old and new back behavior with recents.  While in
             // Overview with live tile enabled, the previous app is technically focused but we
             // add an input consumer to capture all input that would otherwise go to the apps
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 0f36d8e..bcb4559 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -57,6 +57,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.os.Process;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.util.ArraySet;
@@ -1042,8 +1043,9 @@
      * create a new task or bring an existing one into the foreground
      */
     boolean checkActivityAllowedToStart(@Nullable ActivityRecord sourceRecord,
-            @NonNull ActivityRecord targetRecord, boolean newTask, @NonNull Task targetTask,
-            int launchFlags, int balCode, int callingUid, int realCallingUid) {
+            @NonNull ActivityRecord targetRecord, boolean newTask, boolean avoidMoveTaskToFront,
+            @Nullable Task targetTask, int launchFlags, int balCode, int callingUid,
+            int realCallingUid) {
         // BAL Exception allowed in all cases
         if (balCode == BAL_ALLOW_ALLOWLISTED_UID) {
             return true;
@@ -1067,14 +1069,36 @@
         }
 
         if (balCode == BAL_ALLOW_GRACE_PERIOD) {
+            // Allow if launching into new task, and caller matches most recently finished activity
             if (taskToFront && mTopFinishedActivity != null
                     && mTopFinishedActivity.mUid == callingUid) {
                 return true;
-            } else if (!taskToFront) {
-                FinishedActivityEntry finishedEntry =
-                        mTaskIdToFinishedActivity.get(targetTask.mTaskId);
-                if (finishedEntry != null && finishedEntry.mUid == callingUid) {
-                    return true;
+            }
+
+            // Launching into existing task - allow if matches most recently finished activity
+            // within the task.
+            // We can reach here multiple ways:
+            // 1. activity in fg fires intent (taskToFront = false, sourceRecord is available)
+            // 2. activity in bg fires intent (taskToFront = false, sourceRecord is available)
+            // 3. activity in bg fires intent with NEW_FLAG (taskToFront = true,
+            //         avoidMoveTaskToFront = true, sourceRecord is available)
+            // 4. activity in bg fires PI (taskToFront = true, avoidMoveTaskToFront = true,
+            //         sourceRecord is not available, targetTask may be available)
+            if (!taskToFront || avoidMoveTaskToFront) {
+                if (targetTask != null) {
+                    FinishedActivityEntry finishedEntry =
+                            mTaskIdToFinishedActivity.get(targetTask.mTaskId);
+                    if (finishedEntry != null && finishedEntry.mUid == callingUid) {
+                        return true;
+                    }
+                }
+
+                if (sourceRecord != null) {
+                    FinishedActivityEntry finishedEntry =
+                            mTaskIdToFinishedActivity.get(sourceRecord.getTask().mTaskId);
+                    if (finishedEntry != null && finishedEntry.mUid == callingUid) {
+                        return true;
+                    }
                 }
             }
         }
@@ -1098,7 +1122,7 @@
                 bas = isTopActivityMatchingUidAbsentForAsm(taskToCheck, sourceRecord.getUid(),
                         sourceRecord);
             }
-        } else if (!taskToFront) {
+        } else if (targetTask != null && (!taskToFront || avoidMoveTaskToFront)) {
             // We don't have a sourceRecord, and we're launching into an existing task.
             // Allow if callingUid is top of stack.
             bas = isTopActivityMatchingUidAbsentForAsm(targetTask, callingUid,
@@ -1111,12 +1135,14 @@
 
         // ASM rules have failed. Log why
         return logAsmFailureAndCheckFeatureEnabled(sourceRecord, callingUid, realCallingUid,
-                newTask, targetTask, targetRecord, balCode, launchFlags, bas, taskToFront);
+                newTask, avoidMoveTaskToFront, targetTask, targetRecord, balCode, launchFlags,
+                bas, taskToFront);
     }
 
     private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord sourceRecord, int callingUid,
-            int realCallingUid, boolean newTask, Task targetTask, ActivityRecord targetRecord,
-            @BalCode int balCode, int launchFlags, BlockActivityStart bas, boolean taskToFront) {
+            int realCallingUid, boolean newTask, boolean avoidMoveTaskToFront, Task targetTask,
+            ActivityRecord targetRecord, @BalCode int balCode, int launchFlags,
+            BlockActivityStart bas, boolean taskToFront) {
 
         ActivityRecord targetTopActivity = targetTask == null ? null
                 : targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop());
@@ -1133,7 +1159,7 @@
 
         String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", sourceRecord,
                 targetRecord, targetTask, targetTopActivity, realCallingUid, balCode,
-                blockActivityStartAndFeatureEnabled, taskToFront);
+                blockActivityStartAndFeatureEnabled, taskToFront, avoidMoveTaskToFront);
 
         FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
                 /* caller_uid */
@@ -1265,7 +1291,7 @@
 
             Slog.i(TAG, getDebugInfoForActivitySecurity("Clear Top", sourceRecord, targetRecord,
                     targetTask, targetTaskTop, realCallingUid, balCode, shouldBlockActivityStart,
-                    /* taskToFront */ true));
+                    /* taskToFront */ true, /* avoidMoveTaskToFront */ false));
         }
     }
 
@@ -1379,7 +1405,7 @@
     private BlockActivityStart isTopActivityMatchingUidAbsentForAsm(@NonNull Task task,
             int uid, @Nullable ActivityRecord sourceRecord) {
         // If the source is visible, consider it 'top'.
-        if (sourceRecord != null && sourceRecord.isVisible()) {
+        if (sourceRecord != null && sourceRecord.isVisibleRequested()) {
             return new BlockActivityStart(false, false);
         }
 
@@ -1389,6 +1415,12 @@
             return new BlockActivityStart(false, false);
         }
 
+        // If UID is visible in target task, allow launch
+        if (task.forAllActivities((Predicate<ActivityRecord>)
+                ar -> ar.isUid(uid) && ar.isVisibleRequested())) {
+            return new BlockActivityStart(false, false);
+        }
+
         // Consider the source activity, whether or not it is finishing. Do not consider any other
         // finishing activity.
         Predicate<ActivityRecord> topOfStackPredicate = (ar) -> ar.equals(sourceRecord)
@@ -1480,27 +1512,26 @@
             @Nullable ActivityRecord sourceRecord, @NonNull ActivityRecord targetRecord,
             @Nullable Task targetTask, @Nullable ActivityRecord targetTopActivity,
             int realCallingUid, @BalCode int balCode,
-            boolean blockActivityStartAndFeatureEnabled, boolean taskToFront) {
+            boolean blockActivityStartAndFeatureEnabled, boolean taskToFront,
+            boolean avoidMoveTaskToFront) {
         final String prefix = "[ASM] ";
         Function<ActivityRecord, String> recordToString = (ar) -> {
             if (ar == null) {
                 return null;
             }
-            return (ar == sourceRecord ? " [source]=> "
+
+            return (ar == sourceRecord ?        " [source]=> "
                     : ar == targetTopActivity ? " [ top  ]=> "
-                    : ar == targetRecord ? " [target]=> "
-                    : "         => ")
-                    + ar
-                    + " :: visible=" + ar.isVisible()
-                    + ", finishing=" + ar.isFinishing()
-                    + ", alwaysOnTop=" + ar.isAlwaysOnTop()
-                    + ", taskFragment=" + ar.getTaskFragment();
+                    : ar == targetRecord ?      " [target]=> "
+                    :                           "         => ")
+                    + getDebugStringForActivityRecord(ar);
         };
 
         StringJoiner joiner = new StringJoiner("\n");
         joiner.add(prefix + "------ Activity Security " + action + " Debug Logging Start ------");
         joiner.add(prefix + "Block Enabled: " + blockActivityStartAndFeatureEnabled);
         joiner.add(prefix + "ASM Version: " + ActivitySecurityModelFeatureFlags.ASM_VERSION);
+        joiner.add(prefix + "System Time: " + SystemClock.uptimeMillis());
 
         boolean targetTaskMatchesSourceTask = targetTask != null
                 && sourceRecord != null && sourceRecord.getTask() == targetTask;
@@ -1512,6 +1543,8 @@
             joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage);
         } else {
             joiner.add(prefix + "Source Record: " + recordToString.apply(sourceRecord));
+            joiner.add(prefix + "Source Launch Package: " + sourceRecord.launchedFromPackage);
+            joiner.add(prefix + "Source Launch Intent: " + sourceRecord.intent);
             if (targetTaskMatchesSourceTask) {
                 joiner.add(prefix + "Source/Target Task: " + sourceRecord.getTask());
                 joiner.add(prefix + "Source/Target Task Stack: ");
@@ -1536,7 +1569,30 @@
         joiner.add(prefix + "Target Record: " + recordToString.apply(targetRecord));
         joiner.add(prefix + "Intent: " + targetRecord.intent);
         joiner.add(prefix + "TaskToFront: " + taskToFront);
+        joiner.add(prefix + "AvoidMoveToFront: " + avoidMoveTaskToFront);
         joiner.add(prefix + "BalCode: " + balCodeToString(balCode));
+        joiner.add(prefix + "LastResumedActivity: "
+                       + recordToString.apply(mService.mLastResumedActivity));
+
+        if (mTopFinishedActivity != null) {
+            joiner.add(prefix + "TopFinishedActivity: " + mTopFinishedActivity.mDebugInfo);
+        }
+
+        if (!mTaskIdToFinishedActivity.isEmpty()) {
+            joiner.add(prefix + "TaskIdToFinishedActivity: ");
+            mTaskIdToFinishedActivity.values().forEach(
+                    (fae) -> joiner.add(prefix + "  " + fae.mDebugInfo));
+        }
+
+        if (balCode == BAL_ALLOW_VISIBLE_WINDOW || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW
+                || balCode == BAL_ALLOW_FOREGROUND) {
+            Task task = sourceRecord != null ? sourceRecord.getTask() : targetTask;
+            if (task != null && task.getDisplayArea() != null) {
+                joiner.add(prefix + "Tasks: ");
+                task.getDisplayArea().forAllTasks((Consumer<Task>)
+                        t -> joiner.add(prefix + "   T: " + t.toFullString()));
+            }
+        }
 
         joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------");
         return joiner.toString();
@@ -1620,7 +1676,7 @@
             return;
         }
 
-        if (!finishActivity.mVisibleRequested
+        if (!finishActivity.isVisibleRequested()
                 && finishActivity != finishActivity.getTask().getTopMostActivity()) {
             return;
         }
@@ -1666,10 +1722,22 @@
         }
     }
 
+    private static String getDebugStringForActivityRecord(ActivityRecord ar) {
+        return ar
+                + " :: visible=" + ar.isVisible()
+                + ", visibleRequested=" + ar.isVisibleRequested()
+                + ", finishing=" + ar.finishing
+                + ", alwaysOnTop=" + ar.isAlwaysOnTop()
+                + ", lastLaunchTime=" + ar.lastLaunchTime
+                + ", lastVisibleTime=" + ar.lastVisibleTime
+                + ", taskFragment=" + ar.getTaskFragment();
+    }
+
     private class FinishedActivityEntry {
         int mUid;
         int mTaskId;
         int mLaunchCount;
+        String mDebugInfo;
 
         FinishedActivityEntry(ActivityRecord ar) {
             FinishedActivityEntry entry = mTaskIdToFinishedActivity.get(ar.getTask().mTaskId);
@@ -1677,6 +1745,7 @@
             this.mUid = ar.getUid();
             this.mTaskId = taskId;
             this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1;
+            this.mDebugInfo = getDebugStringForActivityRecord(ar);
 
             mService.mH.postDelayed(() -> {
                 synchronized (mService.mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 63ca592..e2bc59b 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -794,6 +794,9 @@
             }
             mService.mAtmService.mKeyguardController.updateDeferTransitionForAod(
                     mAwake /* waiting */);
+            if (!awake) {
+                mDisplayContent.mWallpaperController.onDisplaySwitchFinished();
+            }
         }
     }
 
@@ -836,7 +839,8 @@
         mRemoteInsetsControllerControlsSystemBars = remoteInsetsControllerControlsSystemBars;
     }
 
-    public void screenTurnedOn(ScreenOnListener screenOnListener) {
+    /** Prepares to turn on screen. The given listener is used to notify that it is ready. */
+    public void screenTurningOn(ScreenOnListener screenOnListener) {
         WindowProcessController visibleDozeUiProcess = null;
         synchronized (mLock) {
             mScreenOnEarly = true;
@@ -858,6 +862,11 @@
         }
     }
 
+    /** It is called after {@link #finishScreenTurningOn}. This runs on PowerManager's thread. */
+    public void screenTurnedOn() {
+        mDisplayContent.mWallpaperController.onDisplaySwitchFinished();
+    }
+
     public void screenTurnedOff() {
         synchronized (mLock) {
             mScreenOnEarly = false;
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index fcc1e5b..f279689 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -148,7 +148,7 @@
 final class LetterboxUiController {
 
     private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE =
-            activityRecord -> activityRecord.fillsParent() && !activityRecord.isFinishing();
+            ActivityRecord::occludesParent;
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
 
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 02b3f15..587cc74 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2783,6 +2783,9 @@
         } else {
             throw new RuntimeException("Create the same sleep token twice: " + token);
         }
+        if (isSwappingDisplay) {
+            display.mWallpaperController.onDisplaySwitchStarted();
+        }
         return token;
     }
 
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index d68f932..0fc62a7 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH;
 import static android.app.WallpaperManager.COMMAND_FREEZE;
 import static android.app.WallpaperManager.COMMAND_UNFREEZE;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -120,6 +121,11 @@
 
     private boolean mShouldOffsetWallpaperCenter;
 
+    /**
+     * Whether the wallpaper has been notified about a physical display switch event is started.
+     */
+    private volatile boolean mIsWallpaperNotifiedOnDisplaySwitch;
+
     private final Consumer<WindowState> mFindWallpapers = w -> {
         if (w.mAttrs.type == TYPE_WALLPAPER) {
             WallpaperWindowToken token = w.mToken.asWallpaperToken();
@@ -1083,6 +1089,52 @@
     }
 
     /**
+     * Notifies the wallpaper that the display turns off when switching physical device. If the
+     * wallpaper is currently visible, its client visibility will be preserved until the display is
+     * confirmed to be off or on.
+     */
+    void onDisplaySwitchStarted() {
+        mIsWallpaperNotifiedOnDisplaySwitch = notifyDisplaySwitch(true /* start */);
+    }
+
+    /**
+     * Called when the screen has finished turning on or the device goes to sleep. This is no-op if
+     * the operation is not part of a display switch.
+     */
+    void onDisplaySwitchFinished() {
+        // The method can be called outside WM lock (turned on), so only acquire lock if needed.
+        // This is to optimize the common cases that regular devices don't have display switch.
+        if (mIsWallpaperNotifiedOnDisplaySwitch) {
+            synchronized (mService.mGlobalLock) {
+                mIsWallpaperNotifiedOnDisplaySwitch = false;
+                notifyDisplaySwitch(false /* start */);
+            }
+        }
+    }
+
+    private boolean notifyDisplaySwitch(boolean start) {
+        boolean notified = false;
+        for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) {
+            final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx);
+            for (int i = token.getChildCount() - 1; i >= 0; i--) {
+                final WindowState w = token.getChildAt(i);
+                if (start && !w.mWinAnimator.getShown()) {
+                    continue;
+                }
+                try {
+                    w.mClient.dispatchWallpaperCommand(COMMAND_DISPLAY_SWITCH, 0 /* x */, 0 /* y */,
+                            start ? 1 : 0 /* use z as start or finish */,
+                            null /* bundle */, false /* sync */);
+                } catch (RemoteException e) {
+                    Slog.w(TAG, "Failed to dispatch COMMAND_DISPLAY_SWITCH " + e);
+                }
+                notified = true;
+            }
+        }
+        return notified;
+    }
+
+    /**
      * Each window can request a zoom, example:
      * - User is in overview, zoomed out.
      * - User also pulls down the shade.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index f8ac8da..9650b8bc 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9156,55 +9156,63 @@
             if (fromWin == null || !fromWin.isFocused()) {
                 return false;
             }
-            final TaskFragment fromFragment = fromWin.getTaskFragment();
-            if (fromFragment == null) {
-                return false;
-            }
-            final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment();
-            if (adjacentFragment == null || adjacentFragment.asTask() != null) {
-                // Don't move the focus to another task.
-                return false;
-            }
-            final Rect fromBounds = fromFragment.getBounds();
-            final Rect adjacentBounds = adjacentFragment.getBounds();
-            switch (direction) {
-                case View.FOCUS_LEFT:
-                    if (adjacentBounds.left >= fromBounds.left) {
-                        return false;
-                    }
-                    break;
-                case View.FOCUS_UP:
-                    if (adjacentBounds.top >= fromBounds.top) {
-                        return false;
-                    }
-                    break;
-                case View.FOCUS_RIGHT:
-                    if (adjacentBounds.right <= fromBounds.right) {
-                        return false;
-                    }
-                    break;
-                case View.FOCUS_DOWN:
-                    if (adjacentBounds.bottom <= fromBounds.bottom) {
-                        return false;
-                    }
-                    break;
-                case View.FOCUS_BACKWARD:
-                case View.FOCUS_FORWARD:
-                    // These are not absolute directions. Skip checking the bounds.
-                    break;
-                default:
+            return moveFocusToAdjacentWindow(fromWin, direction);
+        }
+    }
+
+    boolean moveFocusToAdjacentWindow(WindowState fromWin, @FocusDirection int direction) {
+        final TaskFragment fromFragment = fromWin.getTaskFragment();
+        if (fromFragment == null) {
+            return false;
+        }
+        final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment();
+        if (adjacentFragment == null || adjacentFragment.asTask() != null) {
+            // Don't move the focus to another task.
+            return false;
+        }
+        if (adjacentFragment.isIsolatedNav()) {
+            // Don't move the focus if the adjacent TF is isolated navigation.
+            return false;
+        }
+        final Rect fromBounds = fromFragment.getBounds();
+        final Rect adjacentBounds = adjacentFragment.getBounds();
+        switch (direction) {
+            case View.FOCUS_LEFT:
+                if (adjacentBounds.left >= fromBounds.left) {
                     return false;
-            }
-            final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity(
-                    true /* focusableOnly */);
-            if (topRunningActivity == null) {
+                }
+                break;
+            case View.FOCUS_UP:
+                if (adjacentBounds.top >= fromBounds.top) {
+                    return false;
+                }
+                break;
+            case View.FOCUS_RIGHT:
+                if (adjacentBounds.right <= fromBounds.right) {
+                    return false;
+                }
+                break;
+            case View.FOCUS_DOWN:
+                if (adjacentBounds.bottom <= fromBounds.bottom) {
+                    return false;
+                }
+                break;
+            case View.FOCUS_BACKWARD:
+            case View.FOCUS_FORWARD:
+                // These are not absolute directions. Skip checking the bounds.
+                break;
+            default:
                 return false;
-            }
-            moveDisplayToTopInternal(topRunningActivity.getDisplayId());
-            handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity);
-            if (fromWin.isFocused()) {
-                return false;
-            }
+        }
+        final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity(
+                true /* focusableOnly */);
+        if (topRunningActivity == null) {
+            return false;
+        }
+        moveDisplayToTopInternal(topRunningActivity.getDisplayId());
+        handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity);
+        if (fromWin.isFocused()) {
+            return false;
         }
         return true;
     }
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 1c90e30..2049331 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -46,6 +46,7 @@
 #include <com_android_input_flags.h>
 #include <input/Input.h>
 #include <input/PointerController.h>
+#include <input/PrintTools.h>
 #include <input/SpriteController.h>
 #include <inputflinger/InputManager.h>
 #include <limits.h>
@@ -230,10 +231,6 @@
     return a > b ? a : b;
 }
 
-static inline const char* toString(bool value) {
-    return value ? "true" : "false";
-}
-
 static SpriteIcon toSpriteIcon(PointerIcon pointerIcon) {
     // As a minor optimization, do not make a copy of the PointerIcon bitmap here. The loaded
     // PointerIcons are only cached by InputManagerService in java, so we can safely assume they
@@ -288,7 +285,7 @@
     void setSystemUiLightsOut(bool lightsOut);
     void setPointerDisplayId(int32_t displayId);
     void setPointerSpeed(int32_t speed);
-    void setMousePointerAccelerationEnabled(bool enabled);
+    void setMousePointerAccelerationEnabled(int32_t displayId, bool enabled);
     void setTouchpadPointerSpeed(int32_t speed);
     void setTouchpadNaturalScrollingEnabled(bool enabled);
     void setTouchpadTapToClickEnabled(bool enabled);
@@ -304,6 +301,7 @@
     bool setPointerIcon(std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon,
                         int32_t displayId, DeviceId deviceId, int32_t pointerId,
                         const sp<IBinder>& inputToken);
+    void setPointerIconVisibility(int32_t displayId, bool visible);
     void setMotionClassifierEnabled(bool enabled);
     std::optional<std::string> getBluetoothAddress(int32_t deviceId);
     void setStylusButtonMotionEventsEnabled(bool enabled);
@@ -400,8 +398,8 @@
         // Pointer speed.
         int32_t pointerSpeed{0};
 
-        // True if pointer acceleration is enabled for mice.
-        bool mousePointerAccelerationEnabled{true};
+        // Displays on which its associated mice will have pointer acceleration disabled.
+        std::set<int32_t> displaysWithMousePointerAccelerationDisabled{};
 
         // True if pointer gestures are enabled.
         bool pointerGesturesEnabled{true};
@@ -492,8 +490,8 @@
         dump += StringPrintf(INDENT "System UI Lights Out: %s\n",
                              toString(mLocked.systemUiLightsOut));
         dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed);
-        dump += StringPrintf(INDENT "Mouse Pointer Acceleration: %s\n",
-                             mLocked.mousePointerAccelerationEnabled ? "Enabled" : "Disabled");
+        dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n",
+                             dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled).c_str());
         dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n",
                 toString(mLocked.pointerGesturesEnabled));
         dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches));
@@ -676,11 +674,13 @@
         std::scoped_lock _l(mLock);
 
         outConfig->mousePointerSpeed = mLocked.pointerSpeed;
-        outConfig->mousePointerAccelerationEnabled = mLocked.mousePointerAccelerationEnabled;
-        outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed
-                * POINTER_SPEED_EXPONENT);
+        outConfig->displaysWithMousePointerAccelerationDisabled =
+                mLocked.displaysWithMousePointerAccelerationDisabled;
+        outConfig->pointerVelocityControlParameters.scale =
+                exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT);
         outConfig->pointerVelocityControlParameters.acceleration =
-                mLocked.mousePointerAccelerationEnabled
+                mLocked.displaysWithMousePointerAccelerationDisabled.count(
+                        mLocked.pointerDisplayId) == 0
                 ? android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION
                 : 1;
         outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled;
@@ -1224,16 +1224,23 @@
             InputReaderConfiguration::Change::POINTER_SPEED);
 }
 
-void NativeInputManager::setMousePointerAccelerationEnabled(bool enabled) {
+void NativeInputManager::setMousePointerAccelerationEnabled(int32_t displayId, bool enabled) {
     { // acquire lock
         std::scoped_lock _l(mLock);
 
-        if (mLocked.mousePointerAccelerationEnabled == enabled) {
+        const bool oldEnabled =
+                mLocked.displaysWithMousePointerAccelerationDisabled.count(displayId) == 0;
+        if (oldEnabled == enabled) {
             return;
         }
 
-        ALOGI("Setting mouse pointer acceleration to %s", toString(enabled));
-        mLocked.mousePointerAccelerationEnabled = enabled;
+        ALOGI("Setting mouse pointer acceleration to %s on display %d", toString(enabled),
+              displayId);
+        if (enabled) {
+            mLocked.displaysWithMousePointerAccelerationDisabled.erase(displayId);
+        } else {
+            mLocked.displaysWithMousePointerAccelerationDisabled.emplace(displayId);
+        }
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
@@ -1397,6 +1404,13 @@
     return mInputManager->getChoreographer().setPointerIcon(std::move(icon), displayId, deviceId);
 }
 
+void NativeInputManager::setPointerIconVisibility(int32_t displayId, bool visible) {
+    if (!ENABLE_POINTER_CHOREOGRAPHER) {
+        return;
+    }
+    mInputManager->getChoreographer().setPointerIconVisibility(displayId, visible);
+}
+
 TouchAffineTransformation NativeInputManager::getTouchAffineTransformation(
         JNIEnv *env, jfloatArray matrixArr) {
     ATRACE_CALL();
@@ -2168,10 +2182,10 @@
 }
 
 static void nativeSetMousePointerAccelerationEnabled(JNIEnv* env, jobject nativeImplObj,
-                                                     jboolean enabled) {
+                                                     jint displayId, jboolean enabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setMousePointerAccelerationEnabled(enabled);
+    im->setMousePointerAccelerationEnabled(displayId, enabled);
 }
 
 static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) {
@@ -2550,6 +2564,13 @@
                               ibinderForJavaObject(env, inputTokenObj));
 }
 
+static void nativeSetPointerIconVisibility(JNIEnv* env, jobject nativeImplObj, jint displayId,
+                                           jboolean visible) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+    im->setPointerIconVisibility(displayId, visible);
+}
+
 static jboolean nativeCanDispatchToDisplay(JNIEnv* env, jobject nativeImplObj, jint deviceId,
                                            jint displayId) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2791,7 +2812,7 @@
          (void*)nativeTransferTouchFocus},
         {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouch},
         {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
-        {"setMousePointerAccelerationEnabled", "(Z)V",
+        {"setMousePointerAccelerationEnabled", "(IZ)V",
          (void*)nativeSetMousePointerAccelerationEnabled},
         {"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed},
         {"setTouchpadNaturalScrollingEnabled", "(Z)V",
@@ -2828,6 +2849,7 @@
          (void*)nativeSetCustomPointerIcon},
         {"setPointerIcon", "(Landroid/view/PointerIcon;IIILandroid/os/IBinder;)Z",
          (void*)nativeSetPointerIcon},
+        {"setPointerIconVisibility", "(IZ)V", (void*)nativeSetPointerIconVisibility},
         {"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay},
         {"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged},
         {"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation},
diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
index 11c40d7..9c033e2 100644
--- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
+++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
@@ -675,7 +675,8 @@
     options.enableCorrVecOutputs = enableCorrVecOutputs;
     options.intervalMs = intervalMs;
 
-    return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>(),
+    return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>(
+                                                     gnssMeasurementIface->getInterfaceVersion()),
                                              options);
 }
 
diff --git a/services/core/jni/gnss/Gnss.cpp b/services/core/jni/gnss/Gnss.cpp
index 8934c3a..da8928b5 100644
--- a/services/core/jni/gnss/Gnss.cpp
+++ b/services/core/jni/gnss/Gnss.cpp
@@ -196,7 +196,8 @@
 
 jboolean GnssHal::setCallback() {
     if (gnssHalAidl != nullptr) {
-        sp<IGnssCallbackAidl> gnssCbIfaceAidl = new GnssCallbackAidl();
+        sp<IGnssCallbackAidl> gnssCbIfaceAidl =
+                new GnssCallbackAidl(gnssHalAidl->getInterfaceVersion());
         auto status = gnssHalAidl->setCallback(gnssCbIfaceAidl);
         if (!checkAidlStatus(status, "IGnssAidl setCallback() failed.")) {
             return JNI_FALSE;
diff --git a/services/core/jni/gnss/GnssCallback.cpp b/services/core/jni/gnss/GnssCallback.cpp
index 60eed8e6..3d598f7 100644
--- a/services/core/jni/gnss/GnssCallback.cpp
+++ b/services/core/jni/gnss/GnssCallback.cpp
@@ -120,7 +120,7 @@
 
 Status GnssCallbackAidl::gnssSetCapabilitiesCb(const int capabilities) {
     ALOGD("%s: %du\n", __func__, capabilities);
-    bool isAdrCapabilityKnown = (getInterfaceVersion() >= 3) ? true : false;
+    bool isAdrCapabilityKnown = (interfaceVersion >= 3) ? true : false;
     JNIEnv* env = getJniEnv();
     env->CallVoidMethod(mCallbacksObj, method_setTopHalCapabilities, capabilities,
                         isAdrCapabilityKnown);
@@ -178,7 +178,7 @@
 
 Status GnssCallbackAidl::gnssNmeaCb(const int64_t timestamp, const std::string& nmea) {
     // In AIDL v1, if no listener is registered, do not report nmea to the framework.
-    if (getInterfaceVersion() <= 1) {
+    if (interfaceVersion <= 1) {
         if (!isNmeaRegistered) {
             return Status::ok();
         }
diff --git a/services/core/jni/gnss/GnssCallback.h b/services/core/jni/gnss/GnssCallback.h
index 33acec8..0622e53 100644
--- a/services/core/jni/gnss/GnssCallback.h
+++ b/services/core/jni/gnss/GnssCallback.h
@@ -60,6 +60,7 @@
  */
 class GnssCallbackAidl : public hardware::gnss::BnGnssCallback {
 public:
+    GnssCallbackAidl(int version) : interfaceVersion(version){};
     binder::Status gnssSetCapabilitiesCb(const int capabilities) override;
     binder::Status gnssSetSignalTypeCapabilitiesCb(
             const std::vector<android::hardware::gnss::GnssSignalType>& signalTypes) override;
@@ -73,6 +74,9 @@
     binder::Status gnssRequestTimeCb() override;
     binder::Status gnssRequestLocationCb(const bool independentFromGnss,
                                          const bool isUserEmergency) override;
+
+private:
+    const int interfaceVersion;
 };
 
 /*
diff --git a/services/core/jni/gnss/GnssMeasurement.h b/services/core/jni/gnss/GnssMeasurement.h
index 7a95db8..20400fd 100644
--- a/services/core/jni/gnss/GnssMeasurement.h
+++ b/services/core/jni/gnss/GnssMeasurement.h
@@ -41,6 +41,7 @@
             const std::unique_ptr<GnssMeasurementCallback>& callback,
             const android::hardware::gnss::IGnssMeasurementInterface::Options& options) = 0;
     virtual jboolean close() = 0;
+    virtual int getInterfaceVersion() = 0;
 };
 
 class GnssMeasurement : public GnssMeasurementInterface {
@@ -50,6 +51,9 @@
             const std::unique_ptr<GnssMeasurementCallback>& callback,
             const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override;
     jboolean close() override;
+    int getInterfaceVersion() override {
+        return mIGnssMeasurement->getInterfaceVersion();
+    }
 
 private:
     const sp<android::hardware::gnss::IGnssMeasurementInterface> mIGnssMeasurement;
@@ -63,6 +67,9 @@
             const std::unique_ptr<GnssMeasurementCallback>& callback,
             const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override;
     jboolean close() override;
+    int getInterfaceVersion() override {
+        return 0;
+    }
 
 private:
     const sp<android::hardware::gnss::V1_0::IGnssMeasurement> mIGnssMeasurement_V1_0;
diff --git a/services/core/jni/gnss/GnssMeasurementCallback.cpp b/services/core/jni/gnss/GnssMeasurementCallback.cpp
index 2982546..ebab4c3 100644
--- a/services/core/jni/gnss/GnssMeasurementCallback.cpp
+++ b/services/core/jni/gnss/GnssMeasurementCallback.cpp
@@ -392,7 +392,7 @@
 
     jobjectArray gnssAgcArray = nullptr;
     gnssAgcArray = translateAllGnssAgcs(env, data.gnssAgcs);
-    if (this->getInterfaceVersion() >= 3) {
+    if (interfaceVersion >= 3) {
         setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray,
                            /*hasIsFullTracking=*/true, data.isFullTracking);
     } else {
@@ -467,7 +467,7 @@
                                            satellitePvt.tropoDelayMeters);
         }
 
-        if (this->getInterfaceVersion() >= 2) {
+        if (interfaceVersion >= 2) {
             callObjectMethodIgnoringResult(env, satellitePvtBuilderObject,
                                            method_satellitePvtBuilderSetTimeOfClock,
                                            satellitePvt.timeOfClockSeconds);
diff --git a/services/core/jni/gnss/GnssMeasurementCallback.h b/services/core/jni/gnss/GnssMeasurementCallback.h
index b3de486..3cb47ce 100644
--- a/services/core/jni/gnss/GnssMeasurementCallback.h
+++ b/services/core/jni/gnss/GnssMeasurementCallback.h
@@ -53,7 +53,8 @@
 
 class GnssMeasurementCallbackAidl : public hardware::gnss::BnGnssMeasurementCallback {
 public:
-    GnssMeasurementCallbackAidl() : mCallbacksObj(getCallbacksObj()) {}
+    GnssMeasurementCallbackAidl(int version)
+          : mCallbacksObj(getCallbacksObj()), interfaceVersion(version) {}
     android::binder::Status gnssMeasurementCb(const hardware::gnss::GnssData& data) override;
 
 private:
@@ -71,6 +72,7 @@
     void translateGnssClock(JNIEnv* env, const hardware::gnss::GnssData& data, JavaObject& object);
 
     jobject& mCallbacksObj;
+    const int interfaceVersion;
 };
 
 /*
@@ -110,10 +112,10 @@
 
 class GnssMeasurementCallback {
 public:
-    GnssMeasurementCallback() {}
+    GnssMeasurementCallback(int version) : interfaceVersion(version) {}
     sp<GnssMeasurementCallbackAidl> getAidl() {
         if (callbackAidl == nullptr) {
-            callbackAidl = sp<GnssMeasurementCallbackAidl>::make();
+            callbackAidl = sp<GnssMeasurementCallbackAidl>::make(interfaceVersion);
         }
         return callbackAidl;
     }
@@ -128,6 +130,7 @@
 private:
     sp<GnssMeasurementCallbackAidl> callbackAidl;
     sp<GnssMeasurementCallbackHidl> callbackHidl;
+    const int interfaceVersion;
 };
 
 template <class T>
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
index 532823a..e8c5658 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
@@ -17,6 +17,7 @@
 package com.android.server.devicepolicy;
 
 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
+import static android.app.admin.flags.Flags.defaultSmsPersonalAppSuspensionFixEnabled;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.Nullable;
@@ -42,6 +43,7 @@
 import android.view.inputmethod.InputMethodInfo;
 
 import com.android.internal.R;
+import com.android.internal.telephony.SmsApplication;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.utils.Slogf;
 
@@ -97,7 +99,7 @@
         result.removeAll(getSystemLauncherPackages());
         result.removeAll(getAccessibilityServices());
         result.removeAll(getInputMethodPackages());
-        result.remove(Telephony.Sms.getDefaultSmsPackage(mContext));
+        result.remove(getDefaultSmsPackage());
         result.remove(getSettingsPackageName());
 
         final String[] unsuspendablePackages =
@@ -202,6 +204,17 @@
         return resolveInfos != null && !resolveInfos.isEmpty();
     }
 
+    private String getDefaultSmsPackage() {
+        //TODO(b/319449037): Unflag the following change.
+        if (defaultSmsPersonalAppSuspensionFixEnabled()) {
+            return SmsApplication.getDefaultSmsApplicationAsUser(
+                            mContext, /*updateIfNeeded=*/ false, mContext.getUser())
+                    .getPackageName();
+        } else {
+            return Telephony.Sms.getDefaultSmsPackage(mContext);
+        }
+    }
+
 
     void dump(IndentingPrintWriter pw) {
         pw.println("PersonalAppsSuspensionHelper");
@@ -212,7 +225,7 @@
         DevicePolicyManagerService.dumpApps(pw, "accessibility services",
                 getAccessibilityServices());
         DevicePolicyManagerService.dumpApps(pw, "input method packages", getInputMethodPackages());
-        pw.printf("SMS package: %s\n", Telephony.Sms.getDefaultSmsPackage(mContext));
+        pw.printf("SMS package: %s\n", getDefaultSmsPackage());
         pw.printf("Settings package: %s\n", getSettingsPackageName());
         DevicePolicyManagerService.dumpApps(pw, "Packages subject to suspension",
                 getPersonalAppsForSuspension());
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index 62d2d7e..4c74878 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -22,6 +22,7 @@
 import android.content.pm.PermissionInfo
 import android.content.pm.SigningDetails
 import android.os.Build
+import android.permission.flags.Flags
 import android.util.Slog
 import com.android.internal.os.RoSystemProperties
 import com.android.internal.pm.permission.CompatibilityPermissionInfo
@@ -1197,15 +1198,80 @@
             newState.externalState.packageStates[PLATFORM_PACKAGE_NAME]!!
                 .androidPackage!!
                 .signingDetails
-        return sourceSigningDetails?.hasCommonSignerWithCapability(
-            packageSigningDetails,
-            SigningDetails.CertCapabilities.PERMISSION
-        ) == true ||
-            packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) ||
-            platformSigningDetails.checkCapability(
+        val hasCommonSigner =
+            sourceSigningDetails?.hasCommonSignerWithCapability(
                 packageSigningDetails,
                 SigningDetails.CertCapabilities.PERMISSION
-            )
+            ) == true ||
+                packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) ||
+                platformSigningDetails.checkCapability(
+                    packageSigningDetails,
+                    SigningDetails.CertCapabilities.PERMISSION
+                )
+        if (!Flags.signaturePermissionAllowlistEnabled()) {
+            return hasCommonSigner;
+        }
+        if (!hasCommonSigner) {
+            return false
+        }
+        // A platform signature permission also needs to be allowlisted on non-debuggable builds.
+        if (permission.packageName == PLATFORM_PACKAGE_NAME) {
+            val isRequestedByFactoryApp =
+                if (packageState.isSystem) {
+                    // For updated system applications, a signature permission still needs to be
+                    // allowlisted if it wasn't requested by the original application.
+                    if (packageState.isUpdatedSystemApp) {
+                        val disabledSystemPackage =
+                            newState.externalState.disabledSystemPackageStates[
+                                    packageState.packageName]
+                                ?.androidPackage
+                        disabledSystemPackage != null &&
+                            permission.name in disabledSystemPackage.requestedPermissions
+                    } else {
+                        true
+                    }
+                } else {
+                    false
+                }
+            if (
+                !(isRequestedByFactoryApp ||
+                    getSignaturePermissionAllowlistState(packageState, permission.name) == true)
+            ) {
+                Slog.w(
+                    LOG_TAG,
+                    "Signature permission ${permission.name} for package" +
+                        " ${packageState.packageName} (${packageState.path}) not in" +
+                        " signature permission allowlist"
+                )
+                if (!Build.isDebuggable()) {
+                    return false
+                }
+            }
+        }
+        return true
+    }
+
+    private fun MutateStateScope.getSignaturePermissionAllowlistState(
+        packageState: PackageState,
+        permissionName: String
+    ): Boolean? {
+        val permissionAllowlist = newState.externalState.permissionAllowlist
+        val packageName = packageState.packageName
+        return when {
+            packageState.isVendor || packageState.isOdm ->
+                permissionAllowlist.getVendorSignatureAppAllowlistState(packageName, permissionName)
+            packageState.isProduct ->
+                permissionAllowlist.getProductSignatureAppAllowlistState(
+                    packageName,
+                    permissionName
+                )
+            packageState.isSystemExt ->
+                permissionAllowlist.getSystemExtSignatureAppAllowlistState(
+                    packageName,
+                    permissionName
+                )
+            else -> permissionAllowlist.getSignatureAppAllowlistState(packageName, permissionName)
+        }
     }
 
     private fun MutateStateScope.checkPrivilegedPermissionAllowlist(
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index f469ab5..097d73a 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -960,8 +960,8 @@
 
         if (permissionName !in androidPackage.requestedPermissions && oldFlags == 0) {
             if (reportError) {
-                throw SecurityException(
-                    "Permission $permissionName isn't requested by package $packageName"
+                Slog.e(
+                    LOG_TAG, "Permission $permissionName isn't requested by package $packageName"
                 )
             }
             return
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
index 4fcdbfc..d479e52 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
+++ b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
@@ -11,7 +11,6 @@
 // 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 {
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
index 7450607..c99e712 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
+++ b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
@@ -41,17 +41,26 @@
     private static final String MOCK_APK_FILE_1 = "BackgroundInstallControlMockApp1.apk";
     private static final String MOCK_APK_FILE_2 = "BackgroundInstallControlMockApp2.apk";
 
+    // TODO: Move the silent installs to test-app using {@link
+    // BackgroundInstallControlServiceTest#installPackage(String, String)} and remove deviceConfig
+    // branch in BICS.
+    // b/310983905
     @Test
     public void testGetMockBackgroundInstalledPackages() throws Exception {
-        installPackage(TEST_DATA_DIR  + MOCK_APK_FILE_1);
+        installPackage(TEST_DATA_DIR + MOCK_APK_FILE_1);
         installPackage(TEST_DATA_DIR + MOCK_APK_FILE_2);
 
         assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_1)).isNotNull();
         assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_2)).isNotNull();
 
-        assertThat(getDevice().setProperty("debug.transparency.bg-install-apps",
-                    MOCK_PACKAGE_NAME_1 + "," + MOCK_PACKAGE_NAME_2)).isTrue();
-        runDeviceTest("testGetMockBackgroundInstalledPackages");
+        assertThat(
+                getDevice()
+                        .setProperty(
+                                "debug.transparency.bg-install-apps",
+                                MOCK_PACKAGE_NAME_1 + "," + MOCK_PACKAGE_NAME_2))
+                .isTrue();
+        runDeviceTest(
+                "BackgroundInstallControlServiceTest", "testGetMockBackgroundInstalledPackages");
         assertThat(getDevice().uninstallPackage(MOCK_PACKAGE_NAME_1)).isNull();
         assertThat(getDevice().uninstallPackage(MOCK_PACKAGE_NAME_2)).isNull();
 
@@ -65,10 +74,10 @@
         assertThat(result.getStatus() == CommandStatus.SUCCESS).isTrue();
     }
 
-    private void runDeviceTest(String method) throws DeviceNotAvailableException {
+    private void runDeviceTest(String testName, String method) throws DeviceNotAvailableException {
         var options = new DeviceTestRunOptions(PACKAGE_NAME);
-        options.setTestClassName(PACKAGE_NAME + ".BackgroundInstallControlServiceTest");
+        options.setTestClassName(PACKAGE_NAME + "." + testName);
         options.setTestMethodName(method);
         runDeviceTests(options);
     }
-}
+}
\ No newline at end of file
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml
index 1fa1f84..cbe58a8 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml
@@ -24,4 +24,4 @@
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
         android:label="APCT tests for background install control service"
         android:targetPackage="com.android.server.pm.test.app" />
-</manifest>
+</manifest>
\ No newline at end of file
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
index b74e561..b23f591 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
@@ -16,6 +16,10 @@
 
 package com.android.server.pm.test.app;
 
+import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
@@ -23,12 +27,15 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.UserHandle;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.ShellIdentityUtils;
+
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,31 +46,52 @@
 @RunWith(AndroidJUnit4.class)
 public class BackgroundInstallControlServiceTest {
     private static final String TAG = "BackgroundInstallControlServiceTest";
+    private static final String MOCK_PACKAGE_NAME = "com.android.servicestests.apps.bicmockapp3";
 
     private IBackgroundInstallControlService mIBics;
 
     @Before
     public void setUp() {
-        mIBics = IBackgroundInstallControlService.Stub.asInterface(
-                ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE));
+        mIBics =
+                IBackgroundInstallControlService.Stub.asInterface(
+                        ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE));
         assertThat(mIBics).isNotNull();
     }
 
+    @After
+    public void tearDown() {
+        runShellCommand("pm uninstall " + MOCK_PACKAGE_NAME);
+    }
+
     @Test
     public void testGetMockBackgroundInstalledPackages() throws RemoteException {
-        ParceledListSlice<PackageInfo> slice = mIBics.getBackgroundInstalledPackages(
-                    PackageManager.MATCH_ALL,
-                    UserHandle.USER_ALL);
+        ParceledListSlice<PackageInfo> slice =
+                ShellIdentityUtils.invokeMethodWithShellPermissions(
+                        mIBics,
+                        (bics) -> {
+                            try {
+                                return bics.getBackgroundInstalledPackages(
+                                        PackageManager.MATCH_ALL, Process.myUserHandle()
+                                                .getIdentifier());
+                            } catch (RemoteException e) {
+                                throw new RuntimeException(e);
+                            }
+                        },
+                        GET_BACKGROUND_INSTALLED_PACKAGES);
         assertThat(slice).isNotNull();
 
         var packageList = slice.getList();
         assertThat(packageList).isNotNull();
         assertThat(packageList).hasSize(2);
 
-        var expectedPackageNames = Set.of("com.android.servicestests.apps.bicmockapp1",
-                "com.android.servicestests.apps.bicmockapp2");
-        var actualPackageNames = packageList.stream().map((packageInfo) -> packageInfo.packageName)
-                .collect(Collectors.toSet());
+        var expectedPackageNames =
+                Set.of(
+                        "com.android.servicestests.apps.bicmockapp1",
+                        "com.android.servicestests.apps.bicmockapp2");
+        var actualPackageNames =
+                packageList.stream()
+                        .map((packageInfo) -> packageInfo.packageName)
+                        .collect(Collectors.toSet());
         assertThat(actualPackageNames).containsExactlyElementsIn(expectedPackageNames);
     }
-}
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
similarity index 90%
rename from services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
index d82e6ab..0edb3df 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -26,18 +26,13 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.io.File;
 import java.util.List;
 
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AdditionalSubtypeUtilsTest {
+public final class AdditionalSubtypeUtilsTest {
 
     @Test
     public void testSaveAndLoad() throws Exception {
@@ -60,7 +55,7 @@
         // Save & load.
         AtomicFile atomicFile = new AtomicFile(
                 new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml"));
-        AdditionalSubtypeUtils.saveToFile(allSubtypes, methodMap, atomicFile);
+        AdditionalSubtypeUtils.saveToFile(allSubtypes, InputMethodMap.of(methodMap), atomicFile);
         ArrayMap<String, List<InputMethodSubtype>> loadedSubtypes = new ArrayMap<>();
         AdditionalSubtypeUtils.loadFromFile(loadedSubtypes, atomicFile);
 
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
similarity index 90%
rename from services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
index 6eedeea..b7223d6 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,17 +19,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.util.Arrays;
 import java.util.List;
 
-@SmallTest
-@RunWith(AndroidJUnit4.class)
 public final class HardwareKeyboardShortcutControllerTest {
 
     @Test
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
index 7cbfc52..71752ba 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
@@ -28,6 +28,7 @@
 import android.util.ArrayMap;
 import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
@@ -123,11 +124,11 @@
 
     private List<InputMethodInfo> filterInputMethodServices(List<ResolveInfo> resolveInfoList,
             List<String> enabledComponents) {
-        final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
-        final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
-        InputMethodManagerService.filterInputMethodServices(new ArrayMap<>(), methodMap, methodList,
-                enabledComponents, mContext, resolveInfoList);
-        return methodList;
+        final ArrayMap<String, List<InputMethodSubtype>> emptyAdditionalSubtypeMap =
+                new ArrayMap<>();
+        final InputMethodMap methodMap = InputMethodManagerService.filterInputMethodServices(
+                emptyAdditionalSubtypeMap, enabledComponents, mContext, resolveInfoList);
+        return methodMap.values();
     }
 
     private ResolveInfo createFakeSystemResolveInfo(String packageName, String componentName) {
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index 0884b78..fbe384a 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -28,22 +28,16 @@
 import android.view.inputmethod.InputMethodSubtype;
 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
 
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl;
 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
 
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class InputMethodSubtypeSwitchingControllerTest {
+public final class InputMethodSubtypeSwitchingControllerTest {
     private static final String DUMMY_PACKAGE_NAME = "dummy package name";
     private static final String DUMMY_IME_LABEL = "dummy ime label";
     private static final String DUMMY_SETTING_ACTIVITY_NAME = "";
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
index 9688ef6..2857619 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -41,15 +41,12 @@
 
 import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.inputmethod.StartInputFlags;
 
 import com.google.common.truth.Truth;
 
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -57,9 +54,7 @@
 import java.util.Locale;
 import java.util.Objects;
 
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class InputMethodUtilsTest {
+public final class InputMethodUtilsTest {
     private static final boolean IS_AUX = true;
     private static final boolean IS_DEFAULT = true;
     private static final boolean IS_OVERRIDES_IMPLICITLY_ENABLED_SUBTYPE = true;
@@ -274,7 +269,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_EN_US), imi);
             assertEquals(1, result.size());
             verifyEquality(autoSubtype, result.get(0));
@@ -298,7 +293,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_EN_US), imi);
             assertEquals(2, result.size());
             verifyEquality(nonAutoEnUS, result.get(0));
@@ -322,7 +317,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_EN_GB), imi);
             assertEquals(2, result.size());
             verifyEquality(nonAutoEnGB, result.get(0));
@@ -347,7 +342,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_FR), imi);
             assertEquals(2, result.size());
             verifyEquality(nonAutoFrCA, result.get(0));
@@ -368,7 +363,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_FR_CA), imi);
             assertEquals(2, result.size());
             verifyEquality(nonAutoFrCA, result.get(0));
@@ -390,7 +385,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_JA_JP), imi);
             assertEquals(3, result.size());
             verifyEquality(nonAutoJa, result.get(0));
@@ -412,7 +407,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_JA_JP), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoHi, result.get(0));
@@ -429,7 +424,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_JA_JP), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoEnUS, result.get(0));
@@ -446,7 +441,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_JA_JP), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoEnUS, result.get(0));
@@ -468,7 +463,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(Locale.forLanguageTag("sr-Latn-RS")), imi);
             assertEquals(2, result.size());
             assertThat(nonAutoSrLatn, is(in(result)));
@@ -488,7 +483,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(Locale.forLanguageTag("sr-Cyrl-RS")), imi);
             assertEquals(2, result.size());
             assertThat(nonAutoSrCyrl, is(in(result)));
@@ -514,7 +509,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(
                                     Locale.forLanguageTag("sr-Latn-RS-x-android"),
                                     Locale.forLanguageTag("ja-JP"),
@@ -541,7 +536,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_FIL_PH), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoFil, result.get(0));
@@ -559,7 +554,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_FI), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoJa, result.get(0));
@@ -575,7 +570,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_IN), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoIn, result.get(0));
@@ -589,7 +584,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_ID), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoIn, result.get(0));
@@ -603,7 +598,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_IN), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoId, result.get(0));
@@ -617,7 +612,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_ID), imi);
             assertEquals(1, result.size());
             verifyEquality(nonAutoId, result.get(0));
@@ -639,7 +634,7 @@
                     "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
                     subtypes);
             final ArrayList<InputMethodSubtype> result =
-                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SubtypeUtils.getImplicitlyApplicableSubtypes(
                             new LocaleList(LOCALE_FR, LOCALE_EN_US, LOCALE_JA_JP), imi);
             assertThat(nonAutoFrCA, is(in(result)));
             assertThat(nonAutoEnUS, is(in(result)));
@@ -801,19 +796,22 @@
         {
             final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
             methodMap.put(systemIme.getId(), systemIme);
-            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, null, ""));
+            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap),
+                    null, ""));
         }
 
         // Returns null when the config value is empty.
         {
             final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
             methodMap.put(systemIme.getId(), systemIme);
-            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, "", ""));
+            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), "",
+                    ""));
         }
 
         // Returns null when the configured package doesn't have an IME.
         {
-            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(),
+            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(
+                    InputMethodMap.emptyMap(),
                     systemIme.getPackageName(), ""));
         }
 
@@ -821,7 +819,8 @@
         {
             final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
             methodMap.put(systemIme.getId(), systemIme);
-            assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+            assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(
+                    InputMethodMap.of(methodMap),
                     systemIme.getPackageName(), null));
         }
 
@@ -829,13 +828,15 @@
         {
             final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
             methodMap.put(systemIme.getId(), systemIme);
-            assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+            assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(
+                    InputMethodMap.of(methodMap),
                     systemIme.getPackageName(), ""));
         }
 
         // Returns null when the current default isn't found.
         {
-            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(),
+            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(
+                    InputMethodMap.emptyMap(),
                     systemIme.getPackageName(), systemIme.getId()));
         }
 
@@ -846,7 +847,7 @@
             final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
             methodMap.put(systemIme.getId(), systemIme);
             methodMap.put(secondIme.getId(), secondIme);
-            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap),
                     systemIme.getPackageName(), ""));
         }
 
@@ -857,7 +858,8 @@
             final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
             methodMap.put(systemIme.getId(), systemIme);
             methodMap.put(secondIme.getId(), secondIme);
-            assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+            assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(
+                    InputMethodMap.of(methodMap),
                     systemIme.getPackageName(), systemIme.getId()));
         }
 
@@ -867,7 +869,7 @@
             final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme",
                     "fake.voice0", false /* isSystem */);
             methodMap.put(nonSystemIme.getId(), nonSystemIme);
-            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap),
                     nonSystemIme.getPackageName(), nonSystemIme.getId()));
         }
 
@@ -878,7 +880,7 @@
                     "FakeDefaultAutoVoiceIme", "fake.voice0", false /* isSystem */);
             methodMap.put(systemIme.getId(), systemIme);
             methodMap.put(nonSystemIme.getId(), nonSystemIme);
-            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+            assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap),
                     nonSystemIme.getPackageName(), ""));
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
index 01f8129..d0b46f5 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -22,18 +22,12 @@
 
 import android.os.LocaleList;
 
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
 import java.util.Locale;
 
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LocaleUtilsTest {
+public final class LocaleUtilsTest {
 
     private static final LocaleUtils.LocaleExtractor<Locale> sIdentityMapper = source -> source;
 
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
index 9780440..a185ad9 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
@@ -419,7 +419,7 @@
                 "installerTitle");
         packageUserState.setArchiveState(archiveState);
         assertEquals(archiveState, packageUserState.getArchiveState());
-        assertTrue(archiveState.getArchiveTimeMillis() > currentTimeMillis);
+        assertTrue(archiveState.getArchiveTimeMillis() >= currentTimeMillis);
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
index 4095be7..18dc114 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
@@ -20,11 +20,13 @@
 
 import android.annotation.NonNull;
 import android.app.backup.BackupHelper;
+import android.app.backup.BackupHelperWithLogger;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArraySet;
 
 import static org.mockito.Mockito.when;
@@ -32,7 +34,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.backup.Flags;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -55,6 +60,9 @@
     @Mock
     private PackageManager mPackageManagerMock;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -71,7 +79,7 @@
 
         mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
 
-        assertThat(mSystemBackupAgent.mAddedHelpers)
+        assertThat(mSystemBackupAgent.mAddedHelpersKey)
                 .containsExactly(
                         "account_sync_settings",
                         "preferred_activities",
@@ -96,7 +104,7 @@
 
         mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
 
-        assertThat(mSystemBackupAgent.mAddedHelpers)
+        assertThat(mSystemBackupAgent.mAddedHelpersKey)
                 .containsExactly(
                         "account_sync_settings",
                         "preferred_activities",
@@ -118,7 +126,7 @@
 
         mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
 
-        assertThat(mSystemBackupAgent.mAddedHelpers)
+        assertThat(mSystemBackupAgent.mAddedHelpersKey)
                 .containsExactly(
                         "account_sync_settings",
                         "notifications",
@@ -134,7 +142,7 @@
 
         mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
 
-        assertThat(mSystemBackupAgent.mAddedHelpers)
+        assertThat(mSystemBackupAgent.mAddedHelpersKey)
                 .containsExactly(
                         "account_sync_settings",
                         "preferred_activities",
@@ -147,12 +155,42 @@
                         "companion");
     }
 
+    @Test
+    public void onAddHelperIfEligibleForUser_flagIsOff_helpersHaveNoLogger() {
+        UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
+        when(mUserManagerMock.isProfile()).thenReturn(false);
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_METRICS_SYSTEM_BACKUP_AGENTS);
+
+        mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
+
+        for (BackupHelperWithLogger helper:mSystemBackupAgent.mAddedHelpers){
+            assertThat(helper.isLoggerSet()).isFalse();
+        }
+    }
+
+    @Test
+    public void onAddHelperIfEligibleForUser_flagIsOn_helpersHaveLogger() {
+        UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
+        when(mUserManagerMock.isProfile()).thenReturn(false);
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_METRICS_SYSTEM_BACKUP_AGENTS);
+
+        mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
+
+        for (BackupHelperWithLogger helper:mSystemBackupAgent.mAddedHelpers){
+            assertThat(helper.isLoggerSet()).isTrue();
+        }
+    }
+
     private class TestableSystemBackupAgent extends SystemBackupAgent {
-        final Set<String> mAddedHelpers = new ArraySet<>();
+        final Set<String> mAddedHelpersKey = new ArraySet<>();
+        final Set<BackupHelperWithLogger> mAddedHelpers = new ArraySet<>();
 
         @Override
         public void addHelper(String keyPrefix, BackupHelper helper) {
-            mAddedHelpers.add(keyPrefix);
+            mAddedHelpersKey.add(keyPrefix);
+            if (helper instanceof BackupHelperWithLogger) {
+                mAddedHelpers.add((BackupHelperWithLogger) helper);
+            }
         }
 
         @Override
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 284e491..93a2eef 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -31,13 +31,17 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
 import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 import static com.android.server.job.controllers.FlexibilityController.FLEXIBLE_CONSTRAINTS;
-import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
+import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_APPLIED_CONSTRAINTS;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_DEADLINE_PROXIMITY_LIMIT;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE;
-import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS;
+import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINES;
+import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS;
+import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES;
+import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS;
 import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
@@ -76,6 +80,7 @@
 import android.provider.DeviceConfig;
 import android.util.ArraySet;
 import android.util.EmptyArray;
+import android.util.SparseArray;
 
 import com.android.server.AppSchedulingModuleThread;
 import com.android.server.DeviceIdleInternal;
@@ -96,6 +101,7 @@
 import org.mockito.stubbing.Answer;
 
 import java.time.Clock;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.ZoneOffset;
 import java.util.ArrayList;
@@ -182,7 +188,12 @@
 
         mSourceUid = AppGlobals.getPackageManager().getPackageUid(SOURCE_PACKAGE, 0, 0);
 
-        setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "50,60,70,80");
+        setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
+                "500=50|60|70|80"
+                        + ",400=50|60|70|80"
+                        + ",300=50|60|70|80"
+                        + ",200=50|60|70|80"
+                        + ",100=50|60|70|80");
         setDeviceConfigLong(KEY_DEADLINE_PROXIMITY_LIMIT, 0L);
         setDeviceConfigInt(KEY_APPLIED_CONSTRAINTS, FLEXIBLE_CONSTRAINTS);
         waitForQuietModuleThread();
@@ -200,6 +211,11 @@
         }
     }
 
+    private void advanceElapsedClock(long incrementMs) {
+        JobSchedulerService.sElapsedRealtimeClock = Clock.offset(
+                sElapsedRealtimeClock, Duration.ofMillis(incrementMs));
+    }
+
     private void setDeviceConfigInt(String key, int val) {
         mDeviceConfigPropertiesBuilder.setInt(key, val);
         updateDeviceConfig(key);
@@ -250,9 +266,12 @@
      */
     @Test
     public void testDefaultVariableValues() {
-        assertEquals(Integer.bitCount(FLEXIBLE_CONSTRAINTS),
-                mFlexibilityController.mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS.length
-        );
+        SparseArray<int[]> defaultPercentsToDrop =
+                FlexibilityController.FcConfig.DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS;
+        for (int i = 0; i < defaultPercentsToDrop.size(); ++i) {
+            assertEquals(Integer.bitCount(FLEXIBLE_CONSTRAINTS),
+                    defaultPercentsToDrop.valueAt(i).length);
+        }
     }
 
     @Test
@@ -379,10 +398,13 @@
     @Test
     public void testOnConstantsUpdated_FallbackDeadline() {
         JobStatus js = createJobStatus("testFallbackDeadlineConfig", createJob(0));
-        assertEquals(DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0L));
-        setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 100L);
-        assertEquals(100L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0L));
+        final long nowElapsed = sElapsedRealtimeClock.millis();
+        assertEquals(DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.get(JobInfo.PRIORITY_DEFAULT),
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0L));
+        setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 123L);
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES,
+                "500=500,400=400,300=300,200=200,100=100");
+        assertEquals(300L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0L));
     }
 
     @Test
@@ -392,10 +414,32 @@
         JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb);
         assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
-        setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "10,20,30,40");
+        setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
+                "500=1|2|3|4"
+                        + ",400=5|6|7|8"
+                        + ",300=10|20|30|40"
+                        + ",200=50|51|52|53"
+                        + ",100=54|55|56|57");
         assertArrayEquals(
-                mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS,
-                new int[] {10, 20, 30, 40});
+                mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                        .get(JobInfo.PRIORITY_MAX),
+                new int[]{1, 2, 3, 4});
+        assertArrayEquals(
+                mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                        .get(JobInfo.PRIORITY_HIGH),
+                new int[]{5, 6, 7, 8});
+        assertArrayEquals(
+                mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                        .get(JobInfo.PRIORITY_DEFAULT),
+                new int[]{10, 20, 30, 40});
+        assertArrayEquals(
+                mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                        .get(JobInfo.PRIORITY_LOW),
+                new int[]{50, 51, 52, 53});
+        assertArrayEquals(
+                mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
+                        .get(JobInfo.PRIORITY_MIN),
+                new int[]{54, 55, 56, 57});
         assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
         js.setNumDroppedFlexibleConstraints(1);
@@ -408,25 +452,64 @@
 
     @Test
     public void testOnConstantsUpdated_PercentsToDropConstraintsInvalidValues() {
-        JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
-        JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb);
-        js.enqueueTime = JobSchedulerService.sElapsedRealtimeClock.millis();
-        assertEquals(js.enqueueTime + HOUR_IN_MILLIS / 2,
-                mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
-        setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "10,20a,030,40");
-        assertEquals(js.enqueueTime + HOUR_IN_MILLIS / 2,
-                mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
-        setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "10,40");
-        assertEquals(js.enqueueTime + HOUR_IN_MILLIS / 2,
-                mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
-        setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "50,40,10,40");
-        assertEquals(js.enqueueTime + HOUR_IN_MILLIS / 2,
-                mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
+        // No priority mapping
+        setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, "10,20,30,40");
+        final SparseArray<int[]> defaultPercentsToDrop =
+                FlexibilityController.FcConfig.DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS;
+        final SparseArray<int[]> percentsToDrop =
+                mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS;
+        for (int i = 0; i < defaultPercentsToDrop.size(); ++i) {
+            assertArrayEquals(defaultPercentsToDrop.valueAt(i), percentsToDrop.valueAt(i));
+        }
+
+        // Invalid priority-percentList string
+        setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
+                "500=10,20a,030,40"
+                        + ",400=20|40|60|80"
+                        + ",300=25|50|75|80"
+                        + ",200=40|50|60|80"
+                        + ",100=20|40|60|80");
+        for (int i = 0; i < defaultPercentsToDrop.size(); ++i) {
+            assertArrayEquals(defaultPercentsToDrop.valueAt(i), percentsToDrop.valueAt(i));
+        }
+
+        // Invalid percentList strings
+        setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
+                "500=10|20a|030|40" // Letters
+                        + ",400=10|40" // Not enough
+                        + ",300=.|50|_|80" // Characters
+                        + ",200=50|40|10|40" // Out of order
+                        + ",100=30|60|90|101"); // Over 100
+        for (int i = 0; i < defaultPercentsToDrop.size(); ++i) {
+            assertArrayEquals(defaultPercentsToDrop.valueAt(i), percentsToDrop.valueAt(i));
+        }
+
+        // Only partially invalid
+        setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
+                "500=10|20a|030|40" // Letters
+                        + ",400=10|40" // Not enough
+                        + ",300=.|50|_|80" // Characters
+                        + ",200=10|20|30|40" // Valid
+                        + ",100=20|40|60|80"); // Valid
+        assertArrayEquals(defaultPercentsToDrop.get(JobInfo.PRIORITY_MAX),
+                percentsToDrop.get(JobInfo.PRIORITY_MAX));
+        assertArrayEquals(defaultPercentsToDrop.get(JobInfo.PRIORITY_HIGH),
+                percentsToDrop.get(JobInfo.PRIORITY_HIGH));
+        assertArrayEquals(defaultPercentsToDrop.get(JobInfo.PRIORITY_DEFAULT),
+                percentsToDrop.get(JobInfo.PRIORITY_DEFAULT));
+        assertArrayEquals(new int[]{10, 20, 30, 40}, percentsToDrop.get(JobInfo.PRIORITY_LOW));
+        assertArrayEquals(new int[]{20, 40, 60, 80}, percentsToDrop.get(JobInfo.PRIORITY_MIN));
     }
 
     @Test
     public void testGetNextConstraintDropTimeElapsedLocked() {
         setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 200 * HOUR_IN_MILLIS);
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES,
+                "500=" + HOUR_IN_MILLIS
+                        + ",400=" + 25 * HOUR_IN_MILLIS
+                        + ",300=" + 50 * HOUR_IN_MILLIS
+                        + ",200=" + 100 * HOUR_IN_MILLIS
+                        + ",100=" + 200 * HOUR_IN_MILLIS);
 
         long nextTimeToDropNumConstraints;
 
@@ -459,34 +542,34 @@
 
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) / 2,
+        assertEquals(FROZEN_TIME + 800000L + (50 * HOUR_IN_MILLIS) / 2,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(1);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) * 6 / 10,
+        assertEquals(FROZEN_TIME + 800000L + (50 * HOUR_IN_MILLIS) * 6 / 10,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(2);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) * 7 / 10,
+        assertEquals(FROZEN_TIME + 800000L + (50 * HOUR_IN_MILLIS) * 7 / 10,
                 nextTimeToDropNumConstraints);
 
         // no delay, no deadline
-        jb = createJob(0);
+        jb = createJob(0).setPriority(JobInfo.PRIORITY_LOW);
         js = createJobStatus("time", jb);
 
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) / 2, nextTimeToDropNumConstraints);
+        assertEquals(FROZEN_TIME + (100 * HOUR_IN_MILLIS) / 2, nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(1);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) * 6 / 10, nextTimeToDropNumConstraints);
+        assertEquals(FROZEN_TIME + (100 * HOUR_IN_MILLIS) * 6 / 10, nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(2);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) * 7 / 10, nextTimeToDropNumConstraints);
+        assertEquals(FROZEN_TIME + (100 * HOUR_IN_MILLIS) * 7 / 10, nextTimeToDropNumConstraints);
 
         // delay, deadline
         jb = createJob(0)
@@ -514,13 +597,13 @@
     @Test
     public void testCurPercent() {
         long deadline = 100 * MINUTE_IN_MILLIS;
-        long nowElapsed;
+        long nowElapsed = FROZEN_TIME;
         JobInfo.Builder jb = createJob(0).setOverrideDeadline(deadline);
         JobStatus js = createJobStatus("time", jb);
 
         assertEquals(FROZEN_TIME, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
         assertEquals(deadline + FROZEN_TIME,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, FROZEN_TIME));
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, FROZEN_TIME));
         nowElapsed = FROZEN_TIME + 60 * MINUTE_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
@@ -547,7 +630,8 @@
         assertEquals(FROZEN_TIME + delay,
                 mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
         assertEquals(deadline + FROZEN_TIME,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, FROZEN_TIME + delay));
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed,
+                        FROZEN_TIME + delay));
 
         nowElapsed = FROZEN_TIME + delay + 60 * MINUTE_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
@@ -670,34 +754,63 @@
 
     @Test
     public void testGetLifeCycleEndElapsedLocked_Prefetch() {
+        final long nowElapsed = sElapsedRealtimeClock.millis();
+
         // prefetch no estimate
         JobInfo.Builder jb = createJob(0).setPrefetch(true);
         JobStatus js = createJobStatus("time", jb);
         doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
-        assertEquals(Long.MAX_VALUE, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
+        assertEquals(Long.MAX_VALUE,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+
         // prefetch with estimate
         jb = createJob(0).setPrefetch(true);
         js = createJobStatus("time", jb);
         doReturn(1000L).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
-        assertEquals(1000L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
+        assertEquals(1000L,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
     }
 
     @Test
     public void testGetLifeCycleEndElapsedLocked_NonPrefetch() {
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES,
+                "500=" + HOUR_IN_MILLIS
+                        + ",400=" + 2 * HOUR_IN_MILLIS
+                        + ",300=" + 3 * HOUR_IN_MILLIS
+                        + ",200=" + 4 * HOUR_IN_MILLIS
+                        + ",100=" + 5 * HOUR_IN_MILLIS);
+
+        final long nowElapsed = sElapsedRealtimeClock.millis();
+
         // deadline
         JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
         JobStatus js = createJobStatus("time", jb);
         assertEquals(HOUR_IN_MILLIS + FROZEN_TIME,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+
         // no deadline
-        jb = createJob(0);
-        js = createJobStatus("time", jb);
-        assertEquals(FROZEN_TIME + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, 100L));
+        assertEquals(FROZEN_TIME + 2 * HOUR_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_HIGH)),
+                        nowElapsed, 100L));
+        assertEquals(FROZEN_TIME + 3 * HOUR_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)),
+                        nowElapsed, 100L));
+        assertEquals(FROZEN_TIME + 4 * HOUR_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_LOW)),
+                        nowElapsed, 100L));
+        assertEquals(FROZEN_TIME + 5 * HOUR_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_MIN)),
+                        nowElapsed, 100L));
     }
 
     @Test
     public void testGetLifeCycleEndElapsedLocked_Rescheduled() {
+        final long nowElapsed = sElapsedRealtimeClock.millis();
+
         JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
         JobStatus js = createJobStatus("time", jb);
         js = new JobStatus(
@@ -705,20 +818,91 @@
                 0, FROZEN_TIME, FROZEN_TIME);
 
         assertEquals(mFcConfig.RESCHEDULED_JOB_DEADLINE_MS,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
 
         js = new JobStatus(
                 js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 2, /* numSystemStops */ 1,
                 0, FROZEN_TIME, FROZEN_TIME);
 
         assertEquals(2 * mFcConfig.RESCHEDULED_JOB_DEADLINE_MS,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
 
         js = new JobStatus(
                 js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 0, /* numSystemStops */ 10,
                 0, FROZEN_TIME, FROZEN_TIME);
         assertEquals(mFcConfig.MAX_RESCHEDULED_DEADLINE_MS,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+    }
+
+    @Test
+    public void testGetLifeCycleEndElapsedLocked_ScoreAddition() {
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES,
+                "500=" + HOUR_IN_MILLIS
+                        + ",400=" + HOUR_IN_MILLIS
+                        + ",300=" + HOUR_IN_MILLIS
+                        + ",200=" + HOUR_IN_MILLIS
+                        + ",100=" + HOUR_IN_MILLIS);
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES,
+                "500=5,400=4,300=3,200=2,100=1");
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS,
+                "500=" + 5 * MINUTE_IN_MILLIS
+                        + ",400=" + 4 * MINUTE_IN_MILLIS
+                        + ",300=" + 3 * MINUTE_IN_MILLIS
+                        + ",200=" + 2 * MINUTE_IN_MILLIS
+                        + ",100=" + 1 * MINUTE_IN_MILLIS);
+
+        final long nowElapsed = sElapsedRealtimeClock.millis();
+
+        JobStatus jsMax = createJobStatus("testScoreCalculation",
+                createJob(0).setExpedited(true).setPriority(JobInfo.PRIORITY_MAX));
+        JobStatus jsHigh = createJobStatus("testScoreCalculation",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
+        JobStatus jsDefault = createJobStatus("testScoreCalculation",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT));
+        JobStatus jsLow = createJobStatus("testScoreCalculation",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW));
+        JobStatus jsMin = createJobStatus("testScoreCalculation",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN));
+        // Make score = 15
+        mFlexibilityController.prepareForExecutionLocked(jsMax);
+        mFlexibilityController.prepareForExecutionLocked(jsHigh);
+        mFlexibilityController.prepareForExecutionLocked(jsDefault);
+        mFlexibilityController.prepareForExecutionLocked(jsLow);
+        mFlexibilityController.prepareForExecutionLocked(jsMin);
+
+        // deadline
+        JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
+        JobStatus js = createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", jb);
+        assertEquals(HOUR_IN_MILLIS + FROZEN_TIME,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+
+        final long earliestMs = 123L;
+        // no deadline
+        assertEquals(earliestMs + HOUR_IN_MILLIS + 5 * 15 * MINUTE_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
+                                createJob(0).setExpedited(true).setPriority(JobInfo.PRIORITY_MAX)),
+                        nowElapsed, earliestMs));
+        assertEquals(earliestMs + HOUR_IN_MILLIS + 4 * 15 * MINUTE_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
+                                createJob(0).setPriority(JobInfo.PRIORITY_HIGH)),
+                        nowElapsed, earliestMs));
+        assertEquals(earliestMs + HOUR_IN_MILLIS + 3 * 15 * MINUTE_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
+                                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)),
+                        nowElapsed, earliestMs));
+        assertEquals(earliestMs + HOUR_IN_MILLIS + 2 * 15 * MINUTE_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
+                                createJob(0).setPriority(JobInfo.PRIORITY_LOW)),
+                        nowElapsed, earliestMs));
+        assertEquals(earliestMs + HOUR_IN_MILLIS + 1 * 15 * MINUTE_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
+                                createJob(0).setPriority(JobInfo.PRIORITY_MIN)),
+                        nowElapsed, earliestMs));
     }
 
     @Test
@@ -734,6 +918,14 @@
 
     @Test
     public void testFlexibilityTracker() {
+        setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 100 * HOUR_IN_MILLIS);
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES,
+                "500=" + 100 * HOUR_IN_MILLIS
+                        + ",400=" + 100 * HOUR_IN_MILLIS
+                        + ",300=" + 100 * HOUR_IN_MILLIS
+                        + ",200=" + 100 * HOUR_IN_MILLIS
+                        + ",100=" + 100 * HOUR_IN_MILLIS);
+
         FlexibilityController.FlexibilityTracker flexTracker =
                 mFlexibilityController.new FlexibilityTracker(4);
         // Plus one for jobs with 0 required constraint.
@@ -805,8 +997,7 @@
             assertEquals(1, trackedJobs.get(3).size());
             assertEquals(0, trackedJobs.get(4).size());
 
-            final long nowElapsed = ((DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 2)
-                    + HOUR_IN_MILLIS);
+            final long nowElapsed = 51 * HOUR_IN_MILLIS;
             JobSchedulerService.sElapsedRealtimeClock =
                     Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
@@ -1220,66 +1411,161 @@
 
     @Test
     public void testCalculateNumDroppedConstraints() {
-        JobInfo.Builder jb = createJob(22);
-        JobStatus js = createJobStatus("testCalculateNumDroppedConstraints", jb);
-        long nowElapsed = FROZEN_TIME;
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES,
+                "500=" + HOUR_IN_MILLIS
+                        + ",400=" + 5 * HOUR_IN_MILLIS
+                        + ",300=" + 8 * HOUR_IN_MILLIS
+                        + ",200=" + 10 * HOUR_IN_MILLIS
+                        + ",100=" + 20 * HOUR_IN_MILLIS);
+        setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
+                "500=20|40|60|80"
+                        + ",400=20|40|60|80"
+                        + ",300=25|50|75|80"
+                        + ",200=40|50|60|80"
+                        + ",100=20|40|60|80");
 
-        mFlexibilityController.mFlexibilityTracker.add(js);
+        JobStatus jsHigh = createJobStatus("testCalculateNumDroppedConstraints",
+                createJob(24).setPriority(JobInfo.PRIORITY_HIGH));
+        JobStatus jsDefault = createJobStatus("testCalculateNumDroppedConstraints",
+                createJob(23).setPriority(JobInfo.PRIORITY_DEFAULT));
+        JobStatus jsLow = createJobStatus("testCalculateNumDroppedConstraints",
+                createJob(22).setPriority(JobInfo.PRIORITY_LOW));
+        JobStatus jsMin = createJobStatus("testCalculateNumDroppedConstraints",
+                createJob(21).setPriority(JobInfo.PRIORITY_MIN));
+        final long startElapsed = FROZEN_TIME;
+        long nowElapsed = startElapsed;
 
-        assertEquals(3, js.getNumRequiredFlexibleConstraints());
-        assertEquals(0, js.getNumDroppedFlexibleConstraints());
-        assertEquals(1, mFlexibilityController
+        mFlexibilityController.mFlexibilityTracker.add(jsHigh);
+        mFlexibilityController.mFlexibilityTracker.add(jsDefault);
+        mFlexibilityController.mFlexibilityTracker.add(jsLow);
+        mFlexibilityController.mFlexibilityTracker.add(jsMin);
+
+        assertEquals(3, jsHigh.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsHigh.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsDefault.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsDefault.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsLow.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsLow.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsMin.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsMin.getNumDroppedFlexibleConstraints());
+        assertEquals(4, mFlexibilityController
                 .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
 
-        nowElapsed += DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 5;
+        nowElapsed = startElapsed + HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
         mFlexibilityController.mFlexibilityTracker
-                .setNumDroppedFlexibleConstraints(js, 1);
+                .calculateNumDroppedConstraints(jsHigh, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsDefault, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsLow, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsMin, nowElapsed);
 
-        assertEquals(2, js.getNumRequiredFlexibleConstraints());
-        assertEquals(1, js.getNumDroppedFlexibleConstraints());
-        assertEquals(1, mFlexibilityController
-                .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
-
-        mFlexibilityController.mFlexibilityTracker.calculateNumDroppedConstraints(js, nowElapsed);
-
-        assertEquals(2, js.getNumRequiredFlexibleConstraints());
-        assertEquals(1, js.getNumDroppedFlexibleConstraints());
-        assertEquals(1, mFlexibilityController
-                .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
-
-        nowElapsed = FROZEN_TIME;
-        JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
-
-        mFlexibilityController.mFlexibilityTracker.calculateNumDroppedConstraints(js, nowElapsed);
-
-        assertEquals(3, js.getNumRequiredFlexibleConstraints());
-        assertEquals(0, js.getNumDroppedFlexibleConstraints());
-        assertEquals(1, mFlexibilityController
+        assertEquals(2, jsHigh.getNumRequiredFlexibleConstraints());
+        assertEquals(1, jsHigh.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsDefault.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsDefault.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsLow.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsLow.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsMin.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsMin.getNumDroppedFlexibleConstraints());
+        assertEquals(3, mFlexibilityController
                 .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
+        assertEquals(1, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
 
-        nowElapsed += DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 9;
+        nowElapsed = startElapsed;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
-        mFlexibilityController.mFlexibilityTracker.calculateNumDroppedConstraints(js, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsHigh, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsDefault, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsLow, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsMin, nowElapsed);
 
-        assertEquals(0, js.getNumRequiredFlexibleConstraints());
-        assertEquals(3, js.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsHigh.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsHigh.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsDefault.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsDefault.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsLow.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsLow.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsMin.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsMin.getNumDroppedFlexibleConstraints());
+        assertEquals(4, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
+        assertEquals(0, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
+        assertEquals(0, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(1).size());
+        assertEquals(0, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(0).size());
 
-        nowElapsed = FROZEN_TIME + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 6;
+        nowElapsed = startElapsed + 3 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
-        mFlexibilityController.mFlexibilityTracker.calculateNumDroppedConstraints(js, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsHigh, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsDefault, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsLow, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsMin, nowElapsed);
 
-        assertEquals(1, js.getNumRequiredFlexibleConstraints());
-        assertEquals(2, js.getNumDroppedFlexibleConstraints());
+        assertEquals(0, jsHigh.getNumRequiredFlexibleConstraints());
+        assertEquals(3, jsHigh.getNumDroppedFlexibleConstraints());
+        assertEquals(2, jsDefault.getNumRequiredFlexibleConstraints());
+        assertEquals(1, jsDefault.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsLow.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsLow.getNumDroppedFlexibleConstraints());
+        assertEquals(3, jsMin.getNumRequiredFlexibleConstraints());
+        assertEquals(0, jsMin.getNumDroppedFlexibleConstraints());
+        assertEquals(2, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
+        assertEquals(1, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
+        assertEquals(0, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(1).size());
+        assertEquals(1, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(0).size());
+
+        nowElapsed = startElapsed + 4 * HOUR_IN_MILLIS;
+        JobSchedulerService.sElapsedRealtimeClock =
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
+
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsHigh, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsDefault, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsLow, nowElapsed);
+        mFlexibilityController.mFlexibilityTracker
+                .calculateNumDroppedConstraints(jsMin, nowElapsed);
+
+        assertEquals(0, jsHigh.getNumRequiredFlexibleConstraints());
+        assertEquals(3, jsHigh.getNumDroppedFlexibleConstraints());
+        assertEquals(1, jsDefault.getNumRequiredFlexibleConstraints());
+        assertEquals(2, jsDefault.getNumDroppedFlexibleConstraints());
+        assertEquals(2, jsLow.getNumRequiredFlexibleConstraints());
+        assertEquals(1, jsLow.getNumDroppedFlexibleConstraints());
+        assertEquals(2, jsMin.getNumRequiredFlexibleConstraints());
+        assertEquals(1, jsMin.getNumDroppedFlexibleConstraints());
+        assertEquals(0, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
+        assertEquals(2, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
         assertEquals(1, mFlexibilityController
                 .mFlexibilityTracker.getJobsByNumRequiredConstraints(1).size());
+        assertEquals(1, mFlexibilityController
+                .mFlexibilityTracker.getJobsByNumRequiredConstraints(0).size());
     }
 
     @Test
@@ -1308,7 +1594,7 @@
                         .get(js.getSourceUserId(), js.getSourcePackageName()));
         assertEquals(150L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
         assertEquals(1150L,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, 150L));
+                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 150L));
         assertEquals(0, mFlexibilityController.getCurPercentOfLifecycleLocked(js, FROZEN_TIME));
         assertEquals(650L, mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js));
@@ -1317,6 +1603,80 @@
                 .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
     }
 
+    @Test
+    public void testScoreCalculation() {
+        JobStatus jsMax = createJobStatus("testScoreCalculation",
+                createJob(0).setExpedited(true).setPriority(JobInfo.PRIORITY_MAX));
+        JobStatus jsHigh = createJobStatus("testScoreCalculation",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
+        JobStatus jsDefault = createJobStatus("testScoreCalculation",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT));
+        JobStatus jsLow = createJobStatus("testScoreCalculation",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW));
+        JobStatus jsMin = createJobStatus("testScoreCalculation",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN));
+
+        long nowElapsed = sElapsedRealtimeClock.millis();
+        assertEquals(0,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        mFlexibilityController.prepareForExecutionLocked(jsMax);
+        assertEquals(5,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+        nowElapsed += 30 * MINUTE_IN_MILLIS;
+        mFlexibilityController.prepareForExecutionLocked(jsMax);
+        assertEquals(10,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        advanceElapsedClock(31 * MINUTE_IN_MILLIS);
+        nowElapsed += 31 * MINUTE_IN_MILLIS;
+        mFlexibilityController.prepareForExecutionLocked(jsHigh);
+        assertEquals(14,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        advanceElapsedClock(2 * HOUR_IN_MILLIS);
+        nowElapsed += 2 * HOUR_IN_MILLIS;
+        mFlexibilityController.prepareForExecutionLocked(jsDefault);
+        assertEquals(17,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        advanceElapsedClock(3 * HOUR_IN_MILLIS);
+        nowElapsed += 3 * HOUR_IN_MILLIS;
+        mFlexibilityController.prepareForExecutionLocked(jsLow);
+        assertEquals(19,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        advanceElapsedClock(3 * HOUR_IN_MILLIS);
+        nowElapsed += 3 * HOUR_IN_MILLIS;
+        mFlexibilityController.prepareForExecutionLocked(jsMin);
+        assertEquals(20,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        advanceElapsedClock(3 * HOUR_IN_MILLIS);
+        nowElapsed += 3 * HOUR_IN_MILLIS;
+        mFlexibilityController.prepareForExecutionLocked(jsMax);
+        assertEquals(25,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+        mFlexibilityController.unprepareFromExecutionLocked(jsMax);
+        assertEquals(20,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        // 24 hours haven't passed yet. The jobs in the first hour bucket should still be included.
+        advanceElapsedClock(12 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS - 1);
+        nowElapsed += 12 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS - 1;
+        assertEquals(20,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+        // Passed the 24 hour mark. The jobs in the first hour bucket should no longer be included.
+        advanceElapsedClock(2);
+        nowElapsed += 2;
+        assertEquals(10,
+                mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed));
+
+    }
+
     /**
      * The beginning of a lifecycle for prefetch jobs includes the cached maximum of the last time
      * the estimated launch time was updated and the last time the app was opened.
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index e6298ee..5bec903 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -569,6 +569,23 @@
     }
 
     @Test
+    public void testAutoLockPrivateProfile() {
+        UserManagerService mSpiedUms = spy(mUms);
+        UserInfo privateProfileUser =
+                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile",
+                        USER_TYPE_PROFILE_PRIVATE, 0, 0, null);
+        Mockito.doNothing().when(mSpiedUms).setQuietModeEnabledAsync(
+                eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), any(),
+                any());
+
+        mSpiedUms.autoLockPrivateSpace();
+
+        Mockito.verify(mSpiedUms).setQuietModeEnabledAsync(
+                eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true),
+                any(), any());
+    }
+
+    @Test
     public void testAutoLockOnDeviceLockForPrivateProfile() {
         mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         UserManagerService mSpiedUms = spy(mUms);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 57c3a1d..95cfc2a 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -22,6 +22,7 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
 import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG;
+import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES;
 
 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -82,6 +83,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.compatibility.common.util.TestUtils;
+import com.android.internal.R;
 import com.android.internal.compat.IPlatformCompat;
 import com.android.server.LocalServices;
 import com.android.server.accessibility.AccessibilityManagerService.AccessibilityDisplayListener;
@@ -857,8 +859,7 @@
     @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void testIsAccessibilityServiceWarningRequired_requiredByDefault() {
         mockManageAccessibilityGranted(mTestableContext);
-        final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
-        info.setComponentName(COMPONENT_NAME);
+        final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME);
 
         assertThat(mA11yms.isAccessibilityServiceWarningRequired(info)).isTrue();
     }
@@ -867,10 +868,9 @@
     @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() {
         mockManageAccessibilityGranted(mTestableContext);
-        final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
-        info_a.setComponentName(COMPONENT_NAME);
-        final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
-        info_b.setComponentName(new ComponentName("package_b", "class_b"));
+        final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME);
+        final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
+                new ComponentName("package_b", "class_b"));
         final AccessibilityUserState userState = mA11yms.getCurrentUserState();
         userState.mEnabledServices.clear();
         userState.mEnabledServices.add(info_b.getComponentName());
@@ -883,12 +883,12 @@
     @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() {
         mockManageAccessibilityGranted(mTestableContext);
-        final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
-        info_a.setComponentName(new ComponentName("package_a", "class_a"));
-        final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
-        info_b.setComponentName(new ComponentName("package_b", "class_b"));
-        final AccessibilityServiceInfo info_c = new AccessibilityServiceInfo();
-        info_c.setComponentName(new ComponentName("package_c", "class_c"));
+        final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"));
+        final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
+                new ComponentName("package_b", "class_b"));
+        final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo(
+                new ComponentName("package_c", "class_c"));
         final AccessibilityUserState userState = mA11yms.getCurrentUserState();
         userState.mAccessibilityButtonTargets.clear();
         userState.mAccessibilityButtonTargets.add(info_b.getComponentName().flattenToString());
@@ -900,6 +900,51 @@
         assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse();
     }
 
+    @Test
+    @RequiresFlagsEnabled({
+            FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG,
+            FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES})
+    public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() {
+        mockManageAccessibilityGranted(mTestableContext);
+        final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"), true);
+        final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
+                new ComponentName("package_b", "class_b"), false);
+        final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo(
+                new ComponentName("package_c", "class_c"), true);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{
+                        info_b.getComponentName().flattenToString(),
+                        info_c.getComponentName().flattenToString()});
+
+        // info_a is not in the allowlist => require the warning
+        assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_a)).isTrue();
+        // info_b is not preinstalled => require the warning
+        assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_b)).isTrue();
+        // info_c is both in the allowlist and preinstalled => do not require the warning
+        assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse();
+    }
+
+    private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
+            ComponentName componentName) {
+        return mockAccessibilityServiceInfo(componentName, false);
+    }
+
+    private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
+            ComponentName componentName,
+            boolean isSystemApp) {
+        AccessibilityServiceInfo accessibilityServiceInfo =
+                Mockito.spy(new AccessibilityServiceInfo());
+        accessibilityServiceInfo.setComponentName(componentName);
+        ResolveInfo mockResolveInfo = Mockito.mock(ResolveInfo.class);
+        when(accessibilityServiceInfo.getResolveInfo()).thenReturn(mockResolveInfo);
+        mockResolveInfo.serviceInfo = Mockito.mock(ServiceInfo.class);
+        mockResolveInfo.serviceInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
+        when(mockResolveInfo.serviceInfo.applicationInfo.isSystemApp()).thenReturn(isSystemApp);
+        return accessibilityServiceInfo;
+    }
+
     // Single package intents can trigger multiple PackageMonitor callbacks.
     // Collect the state of the lock in a set, since tests only care if calls
     // were all locked or all unlocked.
diff --git a/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
index 9c8276a..84c0ab3 100644
--- a/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.audio;
 
+import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE;
 import static android.media.AudioManager.GET_DEVICES_OUTPUTS;
 import static android.media.AudioPlaybackConfiguration.PLAYER_UPDATE_DEVICE_ID;
 import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
@@ -22,6 +23,7 @@
 import static android.media.MediaFormat.KEY_AAC_DRC_EFFECT_TYPE;
 import static android.media.MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION;
 import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL;
+import static android.os.Process.myPid;
 
 import static com.android.server.audio.LoudnessCodecHelper.SPL_RANGE_LARGE;
 import static com.android.server.audio.LoudnessCodecHelper.SPL_RANGE_MEDIUM;
@@ -37,6 +39,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.media.AudioAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
@@ -64,13 +67,16 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Random;
 
 @RunWith(AndroidJUnit4.class)
 @Presubmit
 public class LoudnessCodecHelperTest {
     private static final String TAG = "LoudnessCodecHelperTest";
 
+    private static final int TEST_USAGE = AudioAttributes.USAGE_MEDIA;
+
+    private static final int TEST_CONTENT = AudioAttributes.CONTENT_TYPE_MUSIC;
+
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
@@ -83,94 +89,84 @@
 
     private final int mInitialApcPiid = 1;
 
+    private int mSessionId;
+
     @Before
     public void setUp() throws Exception {
         mLoudnessHelper = new LoudnessCodecHelper(mAudioService);
+        mSessionId = 1;
 
         when(mAudioService.getActivePlaybackConfigurations()).thenReturn(
-                getApcListForPiids(mInitialApcPiid));
+                getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 0));
 
         when(mDispatcher.asBinder()).thenReturn(Mockito.mock(IBinder.class));
     }
 
     @Test
-    public void registerDispatcher_sendsInitialUpdateOnStart() throws Exception {
+    public void registerDispatcher_sendsUpdateOnAddCodec() throws Exception {
         mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
 
-        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
-                List.of(getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4)));
+        mLoudnessHelper.startLoudnessCodecUpdates(mSessionId);
+        mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222,
+                getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D));
 
-        verify(mDispatcher).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), any());
+        verify(mDispatcher).dispatchLoudnessCodecParameterChange(eq(mSessionId), any());
     }
 
     @Test
-    public void unregisterDispatcher_noInitialUpdateOnStart() throws Exception {
+    public void unregisterDispatcher_noUpdateOnAdd() throws Exception {
         mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
         mLoudnessHelper.unregisterLoudnessCodecUpdatesDispatcher(mDispatcher);
 
-        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
-                List.of(getLoudnessInfo(/*isDownmixing=*/false, CODEC_METADATA_TYPE_MPEG_D)));
-
-        verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
-                any());
-    }
-
-    @Test
-    public void addCodecInfo_sendsInitialUpdateAfterStart() throws Exception {
-        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
-
-        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
-                List.of(getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4)));
-        mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid, /*mediaCodecHash=*/222,
+        mLoudnessHelper.startLoudnessCodecUpdates(mSessionId);
+        mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222,
                 getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D));
 
-        verify(mDispatcher, times(2)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+        verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mSessionId),
                 any());
     }
 
     @Test
-    public void addCodecInfoForUnstartedPiid_noUpdateSent() throws Exception {
-        final int newPiid = 2;
+    public void addCodecInfoForDifferentId_noUpdateSent() throws Exception {
+        final int newSessionId = mSessionId + 1;
         mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
 
-        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
-                List.of(getLoudnessInfo(/*isDownmixing=*/true,
-                        CODEC_METADATA_TYPE_MPEG_4)));
-        mLoudnessHelper.addLoudnessCodecInfo(newPiid, /*mediaCodecHash=*/222,
+        mLoudnessHelper.startLoudnessCodecUpdates(mSessionId);
+        mLoudnessHelper.addLoudnessCodecInfo(newSessionId, /*mediaCodecHash=*/222,
                 getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D));
 
-        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+        verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mSessionId),
                 any());
     }
 
     @Test
-    public void updateCodecParameters_updatesOnlyStartedPiids() throws Exception {
-        final int newPiid = 2;
-        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
-
-        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
-                List.of(getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4)));
-        //does not trigger dispatch since active apc list does not contain newPiid
-        mLoudnessHelper.startLoudnessCodecUpdates(newPiid,
-                List.of(getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D)));
-        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
-                any());
-
-        // triggers dispatch for new active apc with newPiid
-        mLoudnessHelper.updateCodecParameters(getApcListForPiids(newPiid));
-        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(newPiid), any());
-    }
-
-    @Test
     public void updateCodecParameters_noStartedPiids_noDispatch() throws Exception {
         mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
-        mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid, /*mediaCodecHash=*/222,
+        mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222,
                 getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D));
 
-        mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+        mLoudnessHelper.updateCodecParameters(
+                getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 1));
 
-        // no dispatch since mInitialApcPiid was not started
-        verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+        // no dispatch since mSessionId was not started
+        verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mSessionId),
+                any());
+    }
+
+    @Test
+    public void updateCodecParameters_dispatchUpdates() throws Exception {
+        final LoudnessCodecInfo info = getLoudnessInfo(/*isDownmixing=*/true,
+                CODEC_METADATA_TYPE_MPEG_4);
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+        mLoudnessHelper.startLoudnessCodecUpdates(mSessionId);
+        mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222, info);
+
+        mLoudnessHelper.updateCodecParameters(
+                getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 1));
+
+        // second dispatch since player configurations were updated
+        verify(mDispatcher, times(2)).dispatchLoudnessCodecParameterChange(eq(mSessionId),
                 any());
     }
 
@@ -180,13 +176,15 @@
                 CODEC_METADATA_TYPE_MPEG_4);
         mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
 
-        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info));
-        mLoudnessHelper.removeLoudnessCodecInfo(mInitialApcPiid, info);
+        mLoudnessHelper.startLoudnessCodecUpdates(mSessionId);
+        mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222, info);
+        mLoudnessHelper.removeLoudnessCodecInfo(mSessionId, info);
 
-        mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+        mLoudnessHelper.updateCodecParameters(
+                getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 1));
 
         // no second dispatch since codec info was removed for updates
-        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mSessionId),
                 any());
     }
 
@@ -196,13 +194,14 @@
                 CODEC_METADATA_TYPE_MPEG_4);
         mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
 
-        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info));
-        mLoudnessHelper.stopLoudnessCodecUpdates(mInitialApcPiid);
+        mLoudnessHelper.startLoudnessCodecUpdates(mSessionId);
+        mLoudnessHelper.stopLoudnessCodecUpdates(mSessionId);
 
-        mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+        mLoudnessHelper.updateCodecParameters(
+                getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 1));
 
         // no second dispatch since piid was removed for updates
-        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+        verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mSessionId),
                 any());
     }
 
@@ -308,23 +307,28 @@
         assertEquals(6, loudnessParameters.getInt(KEY_AAC_DRC_EFFECT_TYPE));
     }
 
-    private List<AudioPlaybackConfiguration> getApcListForPiids(int... piids) {
+    private List<AudioPlaybackConfiguration> getApcListForApcWithPiidSid(int piid, int sessionId,
+            int devIdx) {
         final ArrayList<AudioPlaybackConfiguration> apcList = new ArrayList<>();
 
         AudioDeviceInfo[] devicesStatic = AudioManager.getDevicesStatic(GET_DEVICES_OUTPUTS);
-        assumeTrue(devicesStatic.length > 0);
-        int index = new Random().nextInt(devicesStatic.length);
-        Log.d(TAG, "Out devices number " + devicesStatic.length + ". Picking index " + index);
-        int deviceId = devicesStatic[index].getId();
+        assumeTrue(devIdx < devicesStatic.length);
+        Log.d(TAG, "Out devices number " + devicesStatic.length + ". Picking index " + devIdx);
+        int deviceId = devicesStatic[devIdx].getId();
 
-        for (int piid : piids) {
-            PlayerBase.PlayerIdCard idCard = Mockito.mock(PlayerBase.PlayerIdCard.class);
-            AudioPlaybackConfiguration apc =
-                    new AudioPlaybackConfiguration(idCard, piid, /*uid=*/1, /*pid=*/1);
-            apc.handleStateEvent(PLAYER_UPDATE_DEVICE_ID, deviceId);
+        PlayerBase.PlayerIdCard idCard = Mockito.mock(PlayerBase.PlayerIdCard.class);
+        AudioPlaybackConfiguration apc =
+                new AudioPlaybackConfiguration(idCard, piid, /*uid=*/1, /*pid=*/myPid());
+        apc.handleStateEvent(PLAYER_UPDATE_DEVICE_ID, deviceId);
+        apc.handleSessionIdEvent(sessionId);
+        apc.handleAudioAttributesEvent(new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA)
+                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+                .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_NONE)
+                .build());
 
-            apcList.add(apc);
-        }
+        apcList.add(apc);
+
         return apcList;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java
index a8eace0..c7300bb 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java
@@ -20,6 +20,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.mock;
@@ -33,11 +34,15 @@
 import android.hardware.biometrics.AuthenticateOptions;
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.IBiometricContextListener.FoldState;
+import android.hardware.biometrics.common.DisplayState;
 import android.hardware.biometrics.common.OperationContext;
 import android.hardware.biometrics.common.OperationReason;
 import android.hardware.display.DisplayManagerGlobal;
+import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.TestableContext;
 import android.view.Display;
 import android.view.DisplayInfo;
@@ -72,6 +77,9 @@
     @Rule
     public TestableContext mContext = new TestableContext(
             InstrumentationRegistry.getInstrumentation().getContext());
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Mock
     private IStatusBarService mStatusBarService;
@@ -395,6 +403,37 @@
         }
     }
 
+    @Test
+    public void testSubscribe_thenStartHal() throws RemoteException {
+        Consumer<OperationContext> updateConsumer = mock(Consumer.class);
+        Consumer<OperationContext> startHalConsumer = mock(Consumer.class);
+        AuthenticateOptions options = new FingerprintAuthenticateOptions.Builder().build();
+        OperationContextExt context = mProvider.updateContext(mOpContext, false /* crypto */);
+
+        assertThat(context.getDisplayState()).isEqualTo(DisplayState.UNKNOWN);
+        assertThat(context.getFoldState()).isEqualTo(IBiometricContextListener.FoldState.UNKNOWN);
+
+        mListener.onDisplayStateChanged(DisplayState.LOCKSCREEN);
+        mListener.onFoldChanged(FoldState.FULLY_CLOSED);
+        mProvider.subscribe(context, startHalConsumer, updateConsumer, options);
+
+        assertThat(context.getDisplayState()).isEqualTo(DisplayState.LOCKSCREEN);
+        assertThat(context.getFoldState()).isEqualTo(FoldState.FULLY_CLOSED);
+        verify(updateConsumer, never()).accept(context.toAidlContext());
+        verify(startHalConsumer).accept(context.toAidlContext(options));
+    }
+
+    @Test
+    public void testSubscribe_withInvalidOptions() {
+        Consumer<OperationContext> updateConsumer = mock(Consumer.class);
+        Consumer<OperationContext> startHalConsumer = mock(Consumer.class);
+        AuthenticateOptions options = mock(AuthenticateOptions.class);
+        OperationContextExt context = mProvider.updateContext(mOpContext, false /* crypto */);
+
+        assertThrows(IllegalStateException.class, () -> mProvider.subscribe(
+                context, startHalConsumer, updateConsumer, options));
+    }
+
     private static byte reason(int type) {
         if (type == StatusBarManager.SESSION_BIOMETRIC_PROMPT) {
             return OperationReason.BIOMETRIC_PROMPT;
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 0973d46..5e38010 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -25,6 +25,7 @@
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.HdmiCecLocalDevice.ActiveSource;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -1807,4 +1808,35 @@
         // TV should only send <Give Osd Name> once
         assertEquals(1, Collections.frequency(mNativeWrapper.getResultMessages(), giveOsdName));
     }
+
+    @Test
+    public void initiateCecByWakeupMessage_selectInternalSourceAfterDelay_broadcastsActiveSource() {
+        HdmiCecMessage activeSourceFromTv =
+                HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_WAKE_UP_MESSAGE);
+        mTestLooper.dispatchAll();
+
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        mHdmiCecLocalDeviceTv.deviceSelect(ADDR_TV, new TestCallback());
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(activeSourceFromTv);
+    }
+
+    @Test
+    public void initiateCecByWakeupMessage_selectInternalSource_doesNotBroadcastActiveSource() {
+        HdmiCecMessage activeSourceFromTv =
+                HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_WAKE_UP_MESSAGE);
+        mTestLooper.dispatchAll();
+
+        mHdmiCecLocalDeviceTv.deviceSelect(ADDR_TV, new TestCallback());
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSourceFromTv);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
index daf18ed..8656f60 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
@@ -9,13 +9,16 @@
  *
  * 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.
+ * 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.pm;
 
+import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -27,6 +30,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -97,7 +101,6 @@
     private Looper mLooper;
     private File mFile;
 
-
     @Mock
     private Context mContext;
     @Mock
@@ -108,8 +111,10 @@
     private UsageStatsManagerInternal mUsageStatsManagerInternal;
     @Mock
     private PermissionManagerServiceInternal mPermissionManager;
+
     @Captor
     private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor;
+
     @Captor
     private ArgumentCaptor<UsageEventListener> mUsageEventListenerCaptor;
 
@@ -119,11 +124,12 @@
 
         mTestLooper = new TestLooper();
         mLooper = mTestLooper.getLooper();
-        mFile = new File(
-                InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(),
-                "test");
-        mBackgroundInstallControlService = new BackgroundInstallControlService(
-                new MockInjector(mContext));
+        mFile =
+                new File(
+                        InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(),
+                        "test");
+        mBackgroundInstallControlService =
+                new BackgroundInstallControlService(new MockInjector(mContext));
 
         verify(mUsageStatsManagerInternal).registerListener(mUsageEventListenerCaptor.capture());
         mUsageEventListener = mUsageEventListenerCaptor.getValue();
@@ -143,8 +149,7 @@
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
         mBackgroundInstallControlService.initBackgroundInstalledPackages();
         assertNotNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        assertEquals(0,
-                mBackgroundInstallControlService.getBackgroundInstalledPackages().size());
+        assertEquals(0, mBackgroundInstallControlService.getBackgroundInstalledPackages().size());
     }
 
     @Test
@@ -161,12 +166,9 @@
         // Write test data to the file on the disk.
         try {
             ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
-            long token = protoOutputStream.start(
-                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
-            protoOutputStream.write(
-                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
-            protoOutputStream.write(
-                    BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
+            long token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
+            protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
             protoOutputStream.end(token);
             protoOutputStream.flush();
             atomicFile.finishWrite(fileOutputStream);
@@ -198,20 +200,14 @@
         try {
             ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
 
-            long token = protoOutputStream.start(
-                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
-            protoOutputStream.write(
-                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
-            protoOutputStream.write(
-                    BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
+            long token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
+            protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
             protoOutputStream.end(token);
 
-            token = protoOutputStream.start(
-                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
-            protoOutputStream.write(
-                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2);
-            protoOutputStream.write(
-                    BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1);
+            token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2);
+            protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1);
             protoOutputStream.end(token);
 
             protoOutputStream.flush();
@@ -241,7 +237,7 @@
         // Read the file on the disk to verify
         var packagesInDisk = new SparseSetArray<>();
         AtomicFile atomicFile = new AtomicFile(mFile);
-        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+        try (FileInputStream fileInputStream = atomicFile.openRead()) {
             ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
 
             while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -249,23 +245,25 @@
                         != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
                     continue;
                 }
-                long token = protoInputStream.start(
-                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                long token =
+                        protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                 String packageName = null;
                 int userId = UserHandle.USER_NULL;
                 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (protoInputStream.getFieldNumber()) {
                         case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
-                            packageName = protoInputStream.readString(
-                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            packageName =
+                                    protoInputStream.readString(
+                                            BackgroundInstalledPackageProto.PACKAGE_NAME);
                             break;
                         case (int) BackgroundInstalledPackageProto.USER_ID:
-                            userId = protoInputStream.readInt(
-                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            userId =
+                                    protoInputStream.readInt(
+                                            BackgroundInstalledPackageProto.USER_ID)
+                                            - 1;
                             break;
                         default:
-                            fail("Undefined field in proto: "
-                                    + protoInputStream.getFieldNumber());
+                            fail("Undefined field in proto: " + protoInputStream.getFieldNumber());
                     }
                 }
                 protoInputStream.end(token);
@@ -296,7 +294,7 @@
         // Read the file on the disk to verify
         var packagesInDisk = new SparseSetArray<>();
         AtomicFile atomicFile = new AtomicFile(mFile);
-        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+        try (FileInputStream fileInputStream = atomicFile.openRead()) {
             ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
 
             while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -304,23 +302,25 @@
                         != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
                     continue;
                 }
-                long token = protoInputStream.start(
-                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                long token =
+                        protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                 String packageName = null;
                 int userId = UserHandle.USER_NULL;
                 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (protoInputStream.getFieldNumber()) {
                         case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
-                            packageName = protoInputStream.readString(
-                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            packageName =
+                                    protoInputStream.readString(
+                                            BackgroundInstalledPackageProto.PACKAGE_NAME);
                             break;
                         case (int) BackgroundInstalledPackageProto.USER_ID:
-                            userId = protoInputStream.readInt(
-                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            userId =
+                                    protoInputStream.readInt(
+                                            BackgroundInstalledPackageProto.USER_ID)
+                                            - 1;
                             break;
                         default:
-                            fail("Undefined field in proto: "
-                                    + protoInputStream.getFieldNumber());
+                            fail("Undefined field in proto: " + protoInputStream.getFieldNumber());
                     }
                 }
                 protoInputStream.end(token);
@@ -353,7 +353,7 @@
         // Read the file on the disk to verify
         var packagesInDisk = new SparseSetArray<>();
         AtomicFile atomicFile = new AtomicFile(mFile);
-        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+        try (FileInputStream fileInputStream = atomicFile.openRead()) {
             ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
 
             while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -361,23 +361,25 @@
                         != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
                     continue;
                 }
-                long token = protoInputStream.start(
-                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                long token =
+                        protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                 String packageName = null;
                 int userId = UserHandle.USER_NULL;
                 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (protoInputStream.getFieldNumber()) {
                         case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
-                            packageName = protoInputStream.readString(
-                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            packageName =
+                                    protoInputStream.readString(
+                                            BackgroundInstalledPackageProto.PACKAGE_NAME);
                             break;
                         case (int) BackgroundInstalledPackageProto.USER_ID:
-                            userId = protoInputStream.readInt(
-                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            userId =
+                                    protoInputStream.readInt(
+                                            BackgroundInstalledPackageProto.USER_ID)
+                                            - 1;
                             break;
                         default:
-                            fail("Undefined field in proto: "
-                                    + protoInputStream.getFieldNumber());
+                            fail("Undefined field in proto: " + protoInputStream.getFieldNumber());
                     }
                 }
                 protoInputStream.end(token);
@@ -399,51 +401,55 @@
 
     @Test
     public void testHandleUsageEvent_permissionDenied() {
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PackageManager.PERMISSION_DENIED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, 0);
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_DENIED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, USER_ID_1, INSTALLER_NAME_1, 0);
         mTestLooper.dispatchAll();
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
     }
 
     @Test
     public void testHandleUsageEvent_permissionGranted() {
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, 0);
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, USER_ID_1, INSTALLER_NAME_1, 0);
         mTestLooper.dispatchAll();
-        assertEquals(1,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        assertEquals(
+                1, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
     }
 
     @Test
     public void testHandleUsageEvent_ignoredEvent() {
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.USER_INTERACTION,
-                USER_ID_1, INSTALLER_NAME_1, 0);
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.USER_INTERACTION, USER_ID_1, INSTALLER_NAME_1, 0);
         mTestLooper.dispatchAll();
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
     }
 
     @Test
     public void testHandleUsageEvent_firstActivityResumedHalfTimeFrame() {
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_1);
         mTestLooper.dispatchAll();
 
         var installerForegroundTimeFrames =
@@ -461,14 +467,18 @@
 
     @Test
     public void testHandleUsageEvent_firstActivityResumedOneTimeFrame() {
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
-        generateUsageEvent(Event.ACTIVITY_STOPPED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
         mTestLooper.dispatchAll();
 
         var installerForegroundTimeFrames =
@@ -486,16 +496,23 @@
 
     @Test
     public void testHandleUsageEvent_firstActivityResumedOneAndHalfTimeFrame() {
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
-        generateUsageEvent(Event.ACTIVITY_STOPPED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_3);
         mTestLooper.dispatchAll();
 
         var installerForegroundTimeFrames =
@@ -517,12 +534,13 @@
 
     @Test
     public void testHandleUsageEvent_firstNoneActivityResumed() {
-        assertEquals(0,
-                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(Event.ACTIVITY_STOPPED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        assertEquals(
+                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
         mTestLooper.dispatchAll();
 
         var installerForegroundTimeFrames =
@@ -535,27 +553,26 @@
     }
 
     @Test
-    public void testHandleUsageEvent_packageAddedNoUsageEvent() throws
-            NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedNoUsageEvent()
+            throws NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
-                /* initiatingPackageName = */ INSTALLER_NAME_1,
-                /* initiatingPackageSigningInfo = */ null,
-                /* originatingPackageName = */ null,
-                /* installingPackageName = */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo =
+                new InstallSourceInfo(
+                        /* initiatingPackageName= */ INSTALLER_NAME_1,
+                        /* initiatingPackageSigningInfo= */ null,
+                        /* originatingPackageName= */ null,
+                        /* installingPackageName= */ INSTALLER_NAME_1);
         assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(
-                eq(PACKAGE_NAME_1),
-                any(),
-                anyInt())
-        ).thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
+                .thenReturn(appInfo);
 
-        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
-                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(appInfo,
+        long createTimestamp =
+                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(
+                appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -572,27 +589,26 @@
     }
 
     @Test
-    public void testHandleUsageEvent_packageAddedInsideTimeFrame() throws
-            NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedInsideTimeFrame()
+            throws NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
-                /* initiatingPackageName = */ INSTALLER_NAME_1,
-                /* initiatingPackageSigningInfo = */ null,
-                /* originatingPackageName = */ null,
-                /* installingPackageName = */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo =
+                new InstallSourceInfo(
+                        /* initiatingPackageName= */ INSTALLER_NAME_1,
+                        /* initiatingPackageSigningInfo= */ null,
+                        /* originatingPackageName= */ null,
+                        /* installingPackageName= */ INSTALLER_NAME_1);
         assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(
-                eq(PACKAGE_NAME_1),
-                any(),
-                anyInt())
-        ).thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
+                .thenReturn(appInfo);
 
-        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
-                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(appInfo,
+        long createTimestamp =
+                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(
+                appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -604,12 +620,16 @@
         // The 2 usage events make the package adding inside a time frame.
         // So it's not a background install. Thus, it's null for the return of
         // mBackgroundInstallControlService.getBackgroundInstalledPackages()
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
-        generateUsageEvent(Event.ACTIVITY_STOPPED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -617,27 +637,26 @@
     }
 
     @Test
-    public void testHandleUsageEvent_packageAddedOutsideTimeFrame1() throws
-            NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedOutsideTimeFrame1()
+            throws NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
-                /* initiatingPackageName = */ INSTALLER_NAME_1,
-                /* initiatingPackageSigningInfo = */ null,
-                /* originatingPackageName = */ null,
-                /* installingPackageName = */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo =
+                new InstallSourceInfo(
+                        /* initiatingPackageName= */ INSTALLER_NAME_1,
+                        /* initiatingPackageSigningInfo= */ null,
+                        /* originatingPackageName= */ null,
+                        /* installingPackageName= */ INSTALLER_NAME_1);
         assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(
-                eq(PACKAGE_NAME_1),
-                any(),
-                anyInt())
-        ).thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
+                .thenReturn(appInfo);
 
-        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
-                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(appInfo,
+        long createTimestamp =
+                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(
+                appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -650,12 +669,16 @@
         // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame,
         // it's a background install. Thus, it's not null for the return of
         // mBackgroundInstallControlService.getBackgroundInstalledPackages()
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(Event.ACTIVITY_STOPPED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -665,28 +688,28 @@
         assertEquals(1, packages.size());
         assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
     }
+
     @Test
-    public void testHandleUsageEvent_packageAddedOutsideTimeFrame2() throws
-            NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedOutsideTimeFrame2()
+            throws NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
-                /* initiatingPackageName = */ INSTALLER_NAME_1,
-                /* initiatingPackageSigningInfo = */ null,
-                /* originatingPackageName = */ null,
-                /* installingPackageName = */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo =
+                new InstallSourceInfo(
+                        /* initiatingPackageName= */ INSTALLER_NAME_1,
+                        /* initiatingPackageSigningInfo= */ null,
+                        /* originatingPackageName= */ null,
+                        /* installingPackageName= */ INSTALLER_NAME_1);
         assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(
-                eq(PACKAGE_NAME_1),
-                any(),
-                anyInt())
-        ).thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
+                .thenReturn(appInfo);
 
-        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
-                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(appInfo,
+        long createTimestamp =
+                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(
+                appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -700,12 +723,16 @@
         // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame,
         // it's a background install. Thus, it's not null for the return of
         // mBackgroundInstallControlService.getBackgroundInstalledPackages()
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(Event.ACTIVITY_STOPPED,
-                USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3);
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_2,
+                INSTALLER_NAME_2,
+                USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -715,31 +742,31 @@
         assertEquals(1, packages.size());
         assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
     }
+
     @Test
-    public void testHandleUsageEvent_packageAddedThroughAdb() throws
-            NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedThroughAdb()
+            throws NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
         // This test is a duplicate of testHandleUsageEvent_packageAddedThroughAdb except the
         // initiatingPackageName used to be null but is now "com.android.shell". This test ensures
         // that the behavior is still the same for when the initiatingPackageName is null.
-        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
-                /* initiatingPackageName = */ null,
-                /* initiatingPackageSigningInfo = */ null,
-                /* originatingPackageName = */ null,
-                /* installingPackageName = */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo =
+                new InstallSourceInfo(
+                        /* initiatingPackageName= */ null,
+                        /* initiatingPackageSigningInfo= */ null,
+                        /* originatingPackageName= */ null,
+                        /* installingPackageName= */ INSTALLER_NAME_1);
         // b/265203007
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(
-                eq(PACKAGE_NAME_1),
-                any(),
-                anyInt())
-        ).thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
+                .thenReturn(appInfo);
 
-        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
-                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(appInfo,
+        long createTimestamp =
+                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(
+                appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -751,12 +778,16 @@
         // for ADB installs the initiatingPackageName used to be null, despite being detected
         // as a background install. Since we do not want to treat side-loaded apps as background
         // install getBackgroundInstalledPackages() is expected to return null
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(Event.ACTIVITY_STOPPED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -764,31 +795,31 @@
         var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
         assertNull(packages);
     }
+
     @Test
-    public void testHandleUsageEvent_packageAddedThroughAdb2() throws
-            NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedThroughAdb2()
+            throws NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
         // This test is a duplicate of testHandleUsageEvent_packageAddedThroughAdb except the
         // initiatingPackageName used to be null but is now "com.android.shell". This test ensures
         // that the behavior is still the same after this change.
-        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
-                /* initiatingPackageName = */ "com.android.shell",
-                /* initiatingPackageSigningInfo = */ null,
-                /* originatingPackageName = */ null,
-                /* installingPackageName = */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo =
+                new InstallSourceInfo(
+                        /* initiatingPackageName= */ "com.android.shell",
+                        /* initiatingPackageSigningInfo= */ null,
+                        /* originatingPackageName= */ null,
+                        /* installingPackageName= */ INSTALLER_NAME_1);
         // b/265203007
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(
-                eq(PACKAGE_NAME_1),
-                any(),
-                anyInt())
-        ).thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
+                .thenReturn(appInfo);
 
-        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
-                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(appInfo,
+        long createTimestamp =
+                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(
+                appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -800,12 +831,16 @@
         // for ADB installs the initiatingPackageName is com.android.shell, despite being detected
         // as a background install. Since we do not want to treat side-loaded apps as background
         // install getBackgroundInstalledPackages() is expected to return null
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
-                anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(Event.ACTIVITY_STOPPED,
-                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -813,6 +848,7 @@
         var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
         assertNull(packages);
     }
+
     @Test
     public void testPackageRemoved() {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
@@ -859,8 +895,7 @@
         packages.add(packageInfo2);
         var packageInfo3 = makePackageInfo(PACKAGE_NAME_3);
         packages.add(packageInfo3);
-        doReturn(packages).when(mPackageManager).getInstalledPackagesAsUser(
-                any(), anyInt());
+        doReturn(packages).when(mPackageManager).getInstalledPackagesAsUser(any(), anyInt());
 
         var resultPackages =
                 mBackgroundInstallControlService.getBackgroundInstalledPackages(0L, USER_ID_1);
@@ -870,18 +905,30 @@
         assertFalse(resultPackages.getList().contains(packageInfo3));
     }
 
+    @Test(expected = SecurityException.class)
+    public void enforceCallerPermissionsThrowsSecurityException() {
+        doThrow(new SecurityException("test")).when(mContext)
+                .enforceCallingOrSelfPermission(eq(GET_BACKGROUND_INSTALLED_PACKAGES), anyString());
+
+        mBackgroundInstallControlService.enforceCallerPermissions();
+    }
+
+    @Test
+    public void enforceCallerPermissionsDoesNotThrowSecurityException() {
+        //enforceCallerQueryPackagesPermissions do not throw
+
+        mBackgroundInstallControlService.enforceCallerPermissions();
+    }
+
     /**
      * Mock a usage event occurring.
      *
      * @param usageEventId id of a usage event
-     * @param userId user id of a usage event
-     * @param pkgName package name of a usage event
-     * @param timestamp timestamp of a usage event
+     * @param userId       user id of a usage event
+     * @param pkgName      package name of a usage event
+     * @param timestamp    timestamp of a usage event
      */
-    private void generateUsageEvent(int usageEventId,
-            int userId,
-            String pkgName,
-            long timestamp) {
+    private void generateUsageEvent(int usageEventId, int userId, String pkgName, long timestamp) {
         Event event = new Event(usageEventId, timestamp);
         event.mPackage = pkgName;
         mUsageEventListener.onUsageEvent(userId, event);
diff --git a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java
index 9d56a36..5e11e17 100644
--- a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
 import android.content.rollback.PackageRollbackInfo;
 import android.util.SparseIntArray;
@@ -81,7 +82,8 @@
             + "'installedUsers':[55,79],"
             + "'ceSnapshotInodes':[]}],'isStaged':false,'causePackages':[{'packageName':'hello',"
             + "'longVersionCode':23},{'packageName':'something','longVersionCode':999}],"
-            + "'committedSessionId':45654465},'timestamp':'2019-10-01T12:29:08.855Z',"
+            + "'committedSessionId':45654465, 'rollbackImpactLevel':1},"
+            + "'timestamp':'2019-10-01T12:29:08.855Z',"
             + "'originalSessionId':567,'state':'enabling','apkSessionId':-1,"
             + "'restoreUserDataInProgress':true, 'userId':0,"
             + "'installerPackageName':'some.installer'}";
@@ -138,6 +140,8 @@
         assertThat(rollback.getOriginalSessionId()).isEqualTo(567);
         assertThat(rollback.info.getRollbackId()).isEqualTo(ID);
         assertThat(rollback.info.getPackages()).isEmpty();
+        assertThat(rollback.info.getRollbackImpactLevel()).isEqualTo(
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
         assertThat(rollback.isEnabling()).isTrue();
         assertThat(rollback.getExtensionVersions().toString())
                 .isEqualTo(extensionVersions.toString());
@@ -158,6 +162,8 @@
 
         assertThat(rollback.info.getRollbackId()).isEqualTo(ID);
         assertThat(rollback.info.getPackages()).isEmpty();
+        assertThat(rollback.info.getRollbackImpactLevel()).isEqualTo(
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
         assertThat(rollback.isEnabling()).isTrue();
         assertThat(rollback.getExtensionVersions().toString())
                 .isEqualTo(extensionVersions.toString());
@@ -175,6 +181,7 @@
         origRb.info.getCausePackages().add(new VersionedPackage("com.made.up", 2));
         origRb.info.getCausePackages().add(new VersionedPackage("com.pack.age", 99));
         origRb.info.setCommittedSessionId(123456);
+        origRb.info.setRollbackImpactLevel(PackageManager.ROLLBACK_USER_IMPACT_HIGH);
 
         PackageRollbackInfo pkgInfo1 =
                 new PackageRollbackInfo(new VersionedPackage("com.made.up", 18),
@@ -226,6 +233,7 @@
         expectedRb.info.getCausePackages().add(new VersionedPackage("hello", 23));
         expectedRb.info.getCausePackages().add(new VersionedPackage("something", 999));
         expectedRb.info.setCommittedSessionId(45654465);
+        expectedRb.info.setRollbackImpactLevel(PackageManager.ROLLBACK_USER_IMPACT_HIGH);
 
         PackageRollbackInfo pkgInfo1 = new PackageRollbackInfo(new VersionedPackage("blah", 55),
                 new VersionedPackage("blah1", 50), new ArrayList<>(), new ArrayList<>(),
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index ea948ca..863cda4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -118,12 +118,19 @@
     // Constructors that should be used to create instances of specific classes. Overrides scoring.
     private static final ImmutableMap<Class<?>, Constructor<?>> PREFERRED_CONSTRUCTORS;
 
+    // Setter methods that receive String parameters, but where those Strings represent Uris
+    // (and are visited/validated).
+    private static final ImmutableSet<Method> SETTERS_WITH_STRING_AS_URI;
+
     static {
         try {
             PREFERRED_CONSTRUCTORS = ImmutableMap.of(
                     Notification.Builder.class,
                     Notification.Builder.class.getConstructor(Context.class, String.class));
 
+            SETTERS_WITH_STRING_AS_URI = ImmutableSet.of(
+                    Person.Builder.class.getMethod("setUri", String.class));
+
             EXCLUDED_SETTERS_OVERLOADS = ImmutableMultimap.<Class<?>, Method>builder()
                     .put(RemoteViews.class,
                             // b/245950570: Tries to connect to service and will crash.
@@ -257,7 +264,7 @@
             @Nullable Class<?> styleClass, @Nullable Class<?> extenderClass,
             @Nullable Class<?> actionExtenderClass, boolean includeRemoteViews) {
         SpecialParameterGenerator specialGenerator = new SpecialParameterGenerator(context);
-        Set<Class<?>> excludedClasses = includeRemoteViews
+        ImmutableSet<Class<?>> excludedClasses = includeRemoteViews
                 ? ImmutableSet.of()
                 : ImmutableSet.of(RemoteViews.class);
         Location location = Location.root(Notification.Builder.class);
@@ -294,7 +301,7 @@
     }
 
     private static Object generateObject(Class<?> clazz, Location where,
-            Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
+            ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
         if (excludingClasses.contains(clazz)) {
             throw new IllegalArgumentException(
                     String.format("Asked to generate a %s but it's part of the excluded set (%s)",
@@ -369,7 +376,7 @@
     }
 
     private static Object constructEmpty(Class<?> clazz, Location where,
-            Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
+            ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
         Constructor<?> bestConstructor;
         if (PREFERRED_CONSTRUCTORS.containsKey(clazz)) {
             // Use the preferred constructor.
@@ -431,7 +438,7 @@
     }
 
     private static void invokeAllSetters(Object instance, Location where, boolean allOverloads,
-            boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes,
+            boolean includingVoidMethods, ImmutableSet<Class<?>> excludingParameterTypes,
             SpecialParameterGenerator specialGenerator) {
         for (Method setter : ReflectionUtils.getAllSetters(instance.getClass(), where,
                 allOverloads, includingVoidMethods, excludingParameterTypes)) {
@@ -462,24 +469,34 @@
     }
 
     private static Object[] generateParameters(Executable executable, Location where,
-            Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
+            ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
         Log.i(TAG, "About to generate parameters for " + ReflectionUtils.methodToString(executable)
                 + " in " + where);
         Type[] parameterTypes = executable.getGenericParameterTypes();
         Object[] parameterValues = new Object[parameterTypes.length];
         for (int i = 0; i < parameterTypes.length; i++) {
-            parameterValues[i] = generateParameter(
-                    parameterTypes[i],
+            boolean generateUriAsString = false;
+            Type parameterType = parameterTypes[i];
+            if (SETTERS_WITH_STRING_AS_URI.contains(executable)
+                    && parameterType.equals(String.class)) {
+                generateUriAsString = true;
+            }
+            Object value = generateParameter(
+                    generateUriAsString ? Uri.class : parameterType,
                     where.plus(executable,
                             String.format("[%d,%s]", i, parameterTypes[i].getTypeName())),
                     excludingClasses,
                     specialGenerator);
+            if (generateUriAsString) {
+                value = ((Uri) value).toString();
+            }
+            parameterValues[i] = value;
         }
         return parameterValues;
     }
 
     private static Object generateParameter(Type parameterType, Location where,
-            Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
+            ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
         if (parameterType instanceof Class<?> parameterClass) {
             return generateObject(
                     parameterClass,
@@ -487,7 +504,8 @@
                     excludingClasses,
                     specialGenerator);
         } else if (parameterType instanceof ParameterizedType parameterizedType) {
-            if (parameterizedType.getRawType().equals(List.class)
+            if ((parameterizedType.getRawType().equals(List.class)
+                    || parameterizedType.getRawType().equals(ArrayList.class))
                     && parameterizedType.getActualTypeArguments()[0] instanceof Class<?>) {
                 ArrayList listValue = new ArrayList();
                 for (int i = 0; i < NUM_ELEMENTS_IN_ARRAY; i++) {
@@ -503,12 +521,14 @@
     }
 
     private static class ReflectionUtils {
-        static Set<Class<?>> getConcreteSubclasses(Class<?> clazz, Class<?> containerClass) {
-            return Arrays.stream(containerClass.getDeclaredClasses())
-                    .filter(
-                            innerClass -> clazz.isAssignableFrom(innerClass)
-                                    && !Modifier.isAbstract(innerClass.getModifiers()))
-                    .collect(Collectors.toSet());
+        static ImmutableSet<Class<?>> getConcreteSubclasses(Class<?> clazz,
+                Class<?> containerClass) {
+            return ImmutableSet.copyOf(
+                    Arrays.stream(containerClass.getDeclaredClasses())
+                            .filter(
+                                    innerClass -> clazz.isAssignableFrom(innerClass)
+                                            && !Modifier.isAbstract(innerClass.getModifiers()))
+                            .collect(Collectors.toSet()));
         }
 
         static String methodToString(Executable executable) {
@@ -611,9 +631,16 @@
     }
 
     private static class SpecialParameterGenerator {
+
+        private static final ImmutableSet<Class<?>> INTERESTING_CLASSES_WITH_SPECIAL_GENERATION =
+                ImmutableSet.of(Uri.class, Icon.class, Intent.class, PendingIntent.class,
+                        RemoteViews.class);
+
         private static final ImmutableSet<Class<?>> INTERESTING_CLASSES =
-                ImmutableSet.of(Person.class, Uri.class, Icon.class, Intent.class,
-                        PendingIntent.class, RemoteViews.class);
+                new ImmutableSet.Builder<Class<?>>()
+                        .addAll(INTERESTING_CLASSES_WITH_SPECIAL_GENERATION)
+                        .add(Person.class) // Constructed via reflection, but high-score.
+                        .build();
         private static final ImmutableSet<Class<?>> MOCKED_CLASSES = ImmutableSet.of();
 
         private static final ImmutableMap<Class<?>, Object> PRIMITIVE_VALUES =
@@ -637,7 +664,7 @@
         }
 
         static boolean canGenerate(Class<?> clazz) {
-            return INTERESTING_CLASSES.contains(clazz)
+            return INTERESTING_CLASSES_WITH_SPECIAL_GENERATION.contains(clazz)
                     || MOCKED_CLASSES.contains(clazz)
                     || clazz.equals(Context.class)
                     || clazz.equals(Bundle.class)
@@ -672,17 +699,6 @@
                 return Icon.createWithContentUri(iconUri);
             }
 
-            if (clazz == Person.class) {
-                // TODO(b/310189261): Person.setUri takes a string instead of a URI. We should
-                //  find a way to use the SpecialParameterGenerator instead of this custom one.
-                Uri personUri = generateUri(
-                        where.plus(Person.Builder.class).plus("setUri", String.class));
-                Uri iconUri = generateUri(where.plus(Person.Builder.class).plus("setIcon",
-                        Icon.class).plus(Icon.class).plus("createWithContentUri", Uri.class));
-                return new Person.Builder().setUri(personUri.toString()).setIcon(
-                        Icon.createWithContentUri(iconUri)).setName("John Doe").build();
-            }
-
             if (clazz == Intent.class) {
                 return new Intent("action", generateUri(where.plus(Intent.class)));
             }
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 2486838..25ad7db 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -59,6 +59,7 @@
 import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN;
 import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER;
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
+import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
 
 import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS;
@@ -123,6 +124,7 @@
 import android.os.Process;
 import android.os.SimpleClock;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
@@ -238,15 +240,19 @@
     public TestWithLooperRule mLooperRule = new TestWithLooperRule();
 
     ConditionProviders mConditionProviders;
-    @Mock NotificationManager mNotificationManager;
-    @Mock PackageManager mPackageManager;
+    @Mock
+    NotificationManager mNotificationManager;
+    @Mock
+    PackageManager mPackageManager;
     private Resources mResources;
     private TestableLooper mTestableLooper;
     private final TestClock mTestClock = new TestClock();
     private ZenModeHelper mZenModeHelper;
     private ContentResolver mContentResolver;
-    @Mock DeviceEffectsApplier mDeviceEffectsApplier;
-    @Mock AppOpsManager mAppOps;
+    @Mock
+    DeviceEffectsApplier mDeviceEffectsApplier;
+    @Mock
+    AppOpsManager mAppOps;
     TestableFlagResolver mTestFlagResolver = new TestableFlagResolver();
     ZenModeEventLoggerFake mZenModeEventLogger;
 
@@ -290,7 +296,7 @@
         when(mPackageManager.getPackageUidAsUser(eq(CUSTOM_PKG_NAME), anyInt()))
                 .thenReturn(CUSTOM_PKG_UID);
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
-                new String[] {pkg});
+                new String[]{pkg});
 
         ApplicationInfo appInfoSpy = spy(new ApplicationInfo());
         appInfoSpy.icon = ICON_RES_ID;
@@ -305,24 +311,26 @@
     }
 
     private XmlResourceParser getDefaultConfigParser() throws IOException, XmlPullParserException {
-        String xml = "<zen version=\"8\" user=\"0\">\n"
-                + "<allow calls=\"false\" repeatCallers=\"false\" messages=\"true\" "
-                + "reminders=\"false\" events=\"false\" callsFrom=\"1\" messagesFrom=\"2\" "
-                + "visualScreenOff=\"true\" alarms=\"true\" "
-                + "media=\"true\" system=\"false\" conversations=\"true\""
-                + " conversationsFrom=\"2\"/>\n"
-                + "<automatic ruleId=\"" + EVENTS_DEFAULT_RULE_ID
-                + "\" enabled=\"false\" snoozing=\"false\""
-                + " name=\"Event\" zen=\"1\""
-                + " component=\"android/com.android.server.notification.EventConditionProvider\""
-                + " conditionId=\"condition://android/event?userId=-10000&amp;calendar=&amp;"
+        String xml = "<zen version=\"10\">\n"
+                + "<allow alarms=\"true\" media=\"true\" system=\"false\" calls=\"true\" "
+                + "callsFrom=\"2\" messages=\"true\"\n"
+                + "messagesFrom=\"2\" reminders=\"false\" events=\"false\" "
+                + "repeatCallers=\"true\" convos=\"true\"\n"
+                + "convosFrom=\"2\"/>\n"
+                + "<automatic ruleId=" + EVENTS_DEFAULT_RULE_ID
+                + " enabled=\"false\" snoozing=\"false\""
+                + " name=\"Event\" zen=\"1\"\n"
+                + "  component=\"android/com.android.server.notification.EventConditionProvider\"\n"
+                + "  conditionId=\"condition://android/event?userId=-10000&amp;calendar=&amp;"
                 + "reply=1\"/>\n"
-                + "<automatic ruleId=\"" + SCHEDULE_DEFAULT_RULE_ID + "\" enabled=\"false\""
-                + " snoozing=\"false\" name=\"Sleeping\" zen=\"1\""
-                + " component=\"android/com.android.server.notification.ScheduleConditionProvider\""
-                + " conditionId=\"condition://android/schedule?days=1.2.3.4.5.6.7 &amp;start=22.0"
-                + "&amp;end=7.0&amp;exitAtAlarm=true\"/>"
-                + "<disallow visualEffects=\"511\" />"
+                + "<automatic ruleId=" + SCHEDULE_DEFAULT_RULE_ID + " enabled=\"false\""
+                + " snoozing=\"false\" name=\"Sleeping\"\n zen=\"1\""
+                + " component=\"android/com.android.server.notification"
+                + ".ScheduleConditionProvider\"\n"
+                + " conditionId=\"condition://android/schedule?days=1.2.3.4.5.6.7&amp;start=22.0"
+                + "&amp;end=7.0&amp;exitAtAlarm=true\"/>\n"
+                + "<disallow visualEffects=\"157\" />\n"
+                + "<state areChannelsBypassingDnd=\"false\" />\n"
                 + "</zen>";
         TypedXmlPullParser parser = Xml.newFastPullParser();
         parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), null);
@@ -408,7 +416,7 @@
     @Test
     public void testZenOff_NoMuteApplied() {
         mZenModeHelper.mZenMode = ZEN_MODE_OFF;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
@@ -421,7 +429,7 @@
     @Test
     public void testZenOn_NotificationApplied() {
         mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         // The most permissive policy
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES
@@ -442,7 +450,7 @@
     @Test
     public void testZenOn_StarredCallers_CallTypesBlocked() {
         mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         // The most permissive policy
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES
@@ -462,7 +470,7 @@
     @Test
     public void testZenOn_AllCallers_CallTypesAllowed() {
         mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         // The most permissive policy
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES
@@ -481,7 +489,7 @@
     @Test
     public void testZenOn_AllowAlarmsMedia_NoAlarmMediaMuteApplied() {
         mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
 
@@ -493,7 +501,7 @@
     @Test
     public void testZenOn_DisallowAlarmsMedia_AlarmMediaMuteApplied() {
         mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
         verifyApplyRestrictions(true, true, AudioAttributes.USAGE_ALARM);
@@ -506,7 +514,7 @@
     @Test
     public void testTotalSilence() {
         mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_NO_INTERRUPTIONS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
@@ -525,7 +533,7 @@
     @Test
     public void testAlarmsOnly_alarmMediaMuteNotApplied() {
         mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
 
@@ -545,7 +553,7 @@
     @Test
     public void testAlarmsOnly_callsMuteApplied() {
         mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
 
@@ -559,7 +567,7 @@
     public void testAlarmsOnly_allZenConfigToggledCannotBypass_alarmMuteNotApplied() {
         // Only audio attributes with SUPPRESIBLE_NEVER can bypass
         mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
 
@@ -571,7 +579,7 @@
         // Only audio attributes with SUPPRESIBLE_NEVER can bypass
         // with special case USAGE_ASSISTANCE_SONIFICATION
         mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
 
@@ -592,7 +600,7 @@
 
     @Test
     public void testApplyRestrictions_whitelist_priorityOnlyMode() {
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
         mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
@@ -607,7 +615,7 @@
 
     @Test
     public void testApplyRestrictions_whitelist_alarmsOnlyMode() {
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mZenMode = Global.ZEN_MODE_ALARMS;
         mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
@@ -622,7 +630,7 @@
 
     @Test
     public void testApplyRestrictions_whitelist_totalSilenceMode() {
-        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+        mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
         mZenModeHelper.mZenMode = Global.ZEN_MODE_NO_INTERRUPTIONS;
         mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
         mZenModeHelper.applyRestrictions();
@@ -1007,7 +1015,7 @@
         mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
 
         assertEquals("Config mismatch: current vs expected: "
-                + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected,
+                        + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected,
                 mZenModeHelper.mConfig);
     }
 
@@ -1336,7 +1344,7 @@
         mZenModeHelper.readXml(parser, true, UserHandle.USER_SYSTEM);
 
         assertEquals("Config mismatch: current vs original: "
-                + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original),
+                        + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original),
                 original, mZenModeHelper.mConfig);
         assertEquals(original.hashCode(), mZenModeHelper.mConfig.hashCode());
     }
@@ -1778,6 +1786,225 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testReadXml_onModesApi_noUpgrade() throws Exception {
+        // When reading XML for something that is already on the modes API system, make sure no
+        // rules' policies get changed.
+        setupZenConfig();
+
+        // Shared for rules
+        ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRules = new ArrayMap<>();
+        final ScheduleInfo weeknights = new ScheduleInfo();
+
+        // Custom rule with a custom policy
+        ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule();
+        customRule.enabled = true;
+        customRule.name = "Custom Rule";
+        customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+        customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights);
+        customRule.component = new ComponentName("android", "ScheduleConditionProvider");
+        ZenPolicy policy = new ZenPolicy.Builder()
+                .allowCalls(PEOPLE_TYPE_CONTACTS)
+                .allowAlarms(true)
+                .allowRepeatCallers(false)
+                .build();
+        // Fill in policy fields, since on modes api we do not expect any rules to have unset fields
+        customRule.zenPolicy = mZenModeHelper.getDefaultZenPolicy().overwrittenWith(policy);
+        enabledAutoRules.put("customRule", customRule);
+        mZenModeHelper.mConfig.automaticRules = enabledAutoRules;
+
+        // set version to post-modes-API = 11
+        ByteArrayOutputStream baos = writeXmlAndPurge(11);
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), null);
+        parser.nextTag();
+        mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+        // basic check: global config maintained
+        setupZenConfigMaintained();
+
+        // Find our automatic rules.
+        ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+        assertThat(rules).hasSize(1);
+        assertThat(rules).containsKey("customRule");
+        ZenRule rule = rules.get("customRule");
+        assertThat(rule.zenPolicy).isEqualTo(customRule.zenPolicy);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testReadXml_upgradeToModesApi_makesCustomPolicies() throws Exception {
+        // When reading in an XML file written from a pre-modes-API version, confirm that we create
+        // a custom policy matching the global config for any automatic rule with no specified
+        // policy.
+        setupZenConfig();
+
+        ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>();
+        ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule();
+        final ScheduleInfo weeknights = new ScheduleInfo();
+        customRule.enabled = true;
+        customRule.name = "Custom Rule";
+        customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+        customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights);
+        customRule.component = new ComponentName("android", "ScheduleConditionProvider");
+        enabledAutoRule.put("customRule", customRule);  // no custom policy set
+        mZenModeHelper.mConfig.automaticRules = enabledAutoRule;
+
+        // set version to pre-modes-API = 10
+        ByteArrayOutputStream baos = writeXmlAndPurge(10);
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), null);
+        parser.nextTag();
+        mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+        // basic check: global config maintained
+        setupZenConfigMaintained();
+
+        // Find our automatic rule and check that it has a policy set now
+        ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+        assertThat(rules).hasSize(1);
+        assertThat(rules).containsKey("customRule");
+        ZenRule rule = rules.get("customRule");
+        assertThat(rule.zenPolicy).isNotNull();
+
+        // Check policy values as set up in setupZenConfig() to confirm they match
+        assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_DISALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_DISALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED);
+        assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testReadXml_upgradeToModesApi_fillsInCustomPolicies() throws Exception {
+        // When reading in an XML file written from a pre-modes-API version, confirm that for an
+        // underspecified ZenPolicy, we fill in all of the gaps with things from the global config
+        // in order to maintain consistency of behavior.
+        setupZenConfig();
+
+        ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>();
+        ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule();
+        final ScheduleInfo weeknights = new ScheduleInfo();
+        customRule.enabled = true;
+        customRule.name = "Custom Rule";
+        customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+        customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights);
+        customRule.component = new ComponentName("android", "ScheduleConditionProvider");
+        customRule.zenPolicy = new ZenPolicy.Builder()
+                .allowAlarms(true)
+                .allowMedia(true)
+                .allowRepeatCallers(false)
+                .build();
+        enabledAutoRule.put("customRule", customRule);
+        mZenModeHelper.mConfig.automaticRules = enabledAutoRule;
+
+        // set version to pre-modes-API = 10
+        ByteArrayOutputStream baos = writeXmlAndPurge(10);
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), null);
+        parser.nextTag();
+        mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+        // basic check: global config maintained
+        setupZenConfigMaintained();
+
+        // Find our automatic rule and check that it has a policy set now
+        ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+        assertThat(rules).hasSize(1);
+        assertThat(rules).containsKey("customRule");
+        ZenRule rule = rules.get("customRule");
+        assertThat(rule.zenPolicy).isNotNull();
+
+        // Check unset policy values match values in setupZenConfig().
+        // Check that set policy values match the values set in the policy.
+        assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_DISALLOW);
+
+        // Check that the rest is filled in from the default
+        assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED);
+        assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW);
+        assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testReadXml_upgradeToModesApi_existingDefaultRulesGetCustomPolicy()
+            throws Exception {
+        setupZenConfig();
+
+        // Default rules, if they exist and have no policies, should get a snapshot of the global
+        // policy, even if they are disabled upon upgrade.
+        ArrayMap<String, ZenModeConfig.ZenRule> automaticRules = new ArrayMap<>();
+        ZenModeConfig.ZenRule defaultScheduleRule = new ZenModeConfig.ZenRule();
+        final ScheduleInfo defaultScheduleRuleInfo = new ScheduleInfo();
+        defaultScheduleRule.enabled = false;
+        defaultScheduleRule.name = "Default Schedule Rule";
+        defaultScheduleRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+        defaultScheduleRule.conditionId = ZenModeConfig.toScheduleConditionId(
+                defaultScheduleRuleInfo);
+        defaultScheduleRule.id = ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID;
+        automaticRules.put(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, defaultScheduleRule);
+
+        ZenModeConfig.ZenRule defaultEventRule = new ZenModeConfig.ZenRule();
+        final ScheduleInfo defaultEventRuleInfo = new ScheduleInfo();
+        defaultEventRule.enabled = false;
+        defaultEventRule.name = "Default Event Rule";
+        defaultEventRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+        defaultEventRule.conditionId = ZenModeConfig.toScheduleConditionId(
+                defaultEventRuleInfo);
+        defaultEventRule.id = ZenModeConfig.EVENTS_DEFAULT_RULE_ID;
+        automaticRules.put(ZenModeConfig.EVENTS_DEFAULT_RULE_ID, defaultEventRule);
+
+        mZenModeHelper.mConfig.automaticRules = automaticRules;
+
+        // set previous version
+        ByteArrayOutputStream baos = writeXmlAndPurge(10);
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), null);
+        parser.nextTag();
+        mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+        // check default rules
+        ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+        assertThat(rules.size()).isGreaterThan(0);
+        for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) {
+            assertThat(rules).containsKey(defaultId);
+            ZenRule rule = rules.get(defaultId);
+            assertThat(rule.zenPolicy).isNotNull();
+
+            // Check policy values as set up in setupZenConfig() to confirm they match
+            assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_DISALLOW);
+            assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_DISALLOW);
+            assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW);
+            assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW);
+            assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW);
+            assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED);
+            assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW);
+            assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW);
+            assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW);
+            assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_ALLOW);
+            assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW);
+        }
+    }
+
+    @Test
     public void testCountdownConditionSubscription() throws Exception {
         ZenModeConfig config = new ZenModeConfig();
         mZenModeHelper.mConfig = config;
@@ -2036,6 +2263,69 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testAddAutomaticZenRule_modesApi_fillsInDefaultValues() {
+        // When a new automatic zen rule is added with only some fields filled in, ensure that
+        // all unset fields are filled in with device defaults.
+
+        // Zen rule with null policy: should get entirely the default state
+        AutomaticZenRule zenRule1 = new AutomaticZenRule("name",
+                new ComponentName("android", "ScheduleConditionProvider"),
+                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+        String id1 = mZenModeHelper.addAutomaticZenRule("android", zenRule1,
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+        // Zen rule with partially-filled policy: should get all of the filled fields set, and the
+        // rest filled with default state
+        AutomaticZenRule zenRule2 = new AutomaticZenRule("name",
+                null,
+                new ComponentName(mContext.getPackageName(), "ScheduleConditionProvider"),
+                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                new ZenPolicy.Builder()
+                        .allowCalls(PEOPLE_TYPE_NONE)
+                        .allowMessages(PEOPLE_TYPE_CONTACTS)
+                        .showFullScreenIntent(true)
+                        .build(),
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+        String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2,
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+        // rule 1 should exist
+        assertThat(id1).isNotNull();
+        ZenModeConfig.ZenRule rule1InConfig = mZenModeHelper.mConfig.automaticRules.get(id1);
+        assertThat(rule1InConfig).isNotNull();
+        assertThat(rule1InConfig.zenPolicy).isNotNull();  // we passed in null; it should now not be
+
+        // all of rule 1 should be the device default's policy
+        assertThat(rule1InConfig.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy());
+
+        // rule 2 should exist
+        assertThat(id2).isNotNull();
+        ZenModeConfig.ZenRule rule2InConfig = mZenModeHelper.mConfig.automaticRules.get(id2);
+        assertThat(rule2InConfig).isNotNull();
+
+        // rule 2: values set from the policy itself
+        assertThat(rule2InConfig.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_NONE);
+        assertThat(rule2InConfig.zenPolicy.getPriorityMessageSenders())
+                .isEqualTo(PEOPLE_TYPE_CONTACTS);
+        assertThat(rule2InConfig.zenPolicy.getVisualEffectFullScreenIntent())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+
+        // the rest of rule 2's settings should be the device defaults
+        assertThat(rule2InConfig.zenPolicy.getPriorityConversationSenders())
+                .isEqualTo(CONVERSATION_SENDERS_IMPORTANT);
+        assertThat(rule2InConfig.zenPolicy.getPriorityCategorySystem())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(rule2InConfig.zenPolicy.getPriorityCategoryAlarms())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(rule2InConfig.zenPolicy.getVisualEffectPeek())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(rule2InConfig.zenPolicy.getVisualEffectNotificationList())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);
+    }
+
+    @Test
     public void testSetAutomaticZenRuleState_nullPkg() {
         AutomaticZenRule zenRule = new AutomaticZenRule("name",
                 null,
@@ -2357,6 +2647,68 @@
 
     @Test
     @EnableFlags(Flags.FLAG_MODES_API)
+    public void updateAutomaticZenRule_nullPolicy_doesNothing() {
+        // Test that when updateAutomaticZenRule is called with a null policy, nothing changes
+        // about the existing policy.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setZenPolicy(new ZenPolicy.Builder()
+                                .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars
+                                .build())
+                        .build(),
+                UPDATE_ORIGIN_APP, "reasons", 0);
+
+        mZenModeHelper.updateAutomaticZenRule(ruleId,
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        // no zen policy
+                        .build(),
+                UPDATE_ORIGIN_APP, "reasons", 0);
+
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void updateAutomaticZenRule_overwritesExistingPolicy() {
+        // Test that when updating an automatic zen rule with an existing policy, the newly set
+        // fields overwrite those from the previous policy, but unset fields in the new policy
+        // keep values from the previous one.
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setZenPolicy(new ZenPolicy.Builder()
+                                .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars
+                                .allowAlarms(false)
+                                .allowReminders(true)
+                                .build())
+                        .build(),
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0);
+
+        mZenModeHelper.updateAutomaticZenRule(ruleId,
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setZenPolicy(new ZenPolicy.Builder()
+                                .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
+                                .build())
+                        .build(),
+                UPDATE_ORIGIN_APP, "reasons", 0);
+
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);  // from update
+        assertThat(savedRule.getZenPolicy().getPriorityCallSenders())
+                .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS);  // from update
+        assertThat(savedRule.getZenPolicy().getPriorityCategoryAlarms())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);  // from original
+        assertThat(savedRule.getZenPolicy().getPriorityCategoryReminders())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);  // from original
+    }
+
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
     public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() {
         ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                 ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -2460,7 +2812,8 @@
         DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI);
 
         private final boolean mEnabled;
-        @ConfigChangeOrigin private final int mOriginForUserActionInSystemUi;
+        @ConfigChangeOrigin
+        private final int mOriginForUserActionInSystemUi;
 
         ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) {
             this.mEnabled = enabled;
@@ -2506,7 +2859,7 @@
         //   - rules active = 1
         //   - user action = true (system-based turning zen mode on)
         //   - package uid = system (as set above)
-        //   - resulting DNDPolicyProto the same as the values in setupZenConfig()
+        //   - resulting DNDPolicyProto the same as the values in setupZenConfig() (global policy)
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
         assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
@@ -2600,7 +2953,7 @@
         //   - 1 rule (newly) active
         //   - automatic (is not a user action)
         //   - package UID is written to be the rule *owner* even though it "comes from system"
-        //   - zen policy is the same as the set-up zen config
+        //   - zen policy is the default as it's unspecified
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
         assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
@@ -2609,10 +2962,10 @@
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
         assertFalse(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
-        checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
+        checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0));
 
         // When the automatic rule is disabled, this should turn off zen mode and also count as a
-        // user action.
+        // user action. We don't care what the consolidated policy is when DND turns off.
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
                 mZenModeEventLogger.getEventId(1));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
@@ -2835,28 +3188,28 @@
         // First: turn on rule 1
         mZenModeHelper.setAutomaticZenRuleState(id,
                 new Condition(zenRule.getConditionId(), "", STATE_TRUE),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,  Process.SYSTEM_UID);
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // Second: turn on rule 2
         mZenModeHelper.setAutomaticZenRuleState(id2,
                 new Condition(zenRule2.getConditionId(), "", STATE_TRUE),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,  Process.SYSTEM_UID);
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // Third: turn on rule 3
         mZenModeHelper.setAutomaticZenRuleState(id3,
                 new Condition(zenRule3.getConditionId(), "", STATE_TRUE),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,  Process.SYSTEM_UID);
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // Fourth: Turn *off* rule 2
         mZenModeHelper.setAutomaticZenRuleState(id2,
                 new Condition(zenRule2.getConditionId(), "", STATE_FALSE),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,  Process.SYSTEM_UID);
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // This should result in a total of four events
         assertEquals(4, mZenModeEventLogger.numLoggedChanges());
 
         // Event 1: rule 1 turns on. We expect this to turn on DND (zen mode) overall, so that's
-        // what the event should reflect. At this time, the policy is the same as initial setup.
+        // what the event should reflect. At this time, the policy is the default.
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
         assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
@@ -2864,7 +3217,7 @@
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
         assertFalse(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
-        checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
+        checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0));
 
         // Event 2: rule 2 turns on. This should not change anything about the policy, so the only
         // change is that there are more rules active now.
@@ -2873,7 +3226,7 @@
         assertEquals(2, mZenModeEventLogger.getNumRulesActive(1));
         assertFalse(mZenModeEventLogger.getIsUserAction(1));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1));
-        checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1));
+        checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(1));
 
         // Event 3: rule 3 turns on. This should trigger a policy change, and be classified as such,
         // but meanwhile also change the number of active rules.
@@ -2926,12 +3279,16 @@
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
+        // Explicitly set up all rules with the same policy as the manual rule so there will be
+        // no policy changes in this test case.
+        ZenPolicy manualRulePolicy = mZenModeHelper.mConfig.toZenPolicy();
+
         // Rule 1, owned by a package
         AutomaticZenRule zenRule = new AutomaticZenRule("name",
                 null,
                 new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
                 ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
-                null,
+                manualRulePolicy,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
                 UPDATE_ORIGIN_APP, "test", Process.SYSTEM_UID);
@@ -2941,7 +3298,7 @@
                 null,
                 new ComponentName("android", "ScheduleConditionProvider"),
                 ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
-                null,
+                manualRulePolicy,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
                 modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID);
@@ -3172,7 +3529,8 @@
     }
 
     @Test
-    public void testUpdateConsolidatedPolicy_defaultRulesOnly() {
+    @DisableFlags(Flags.FLAG_MODES_API)
+    public void testUpdateConsolidatedPolicy_preModesApiDefaultRulesOnly_takesGlobalDefault() {
         setupZenConfig();
 
         // When there's one automatic rule active and it doesn't specify a policy, test that the
@@ -3205,12 +3563,39 @@
     }
 
     @Test
-    public void testUpdateConsolidatedPolicy_customPolicyOnly() {
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDeviceDefault() {
+        setupZenConfig();
+
+        // When there's one automatic rule active and it doesn't specify a policy, test that the
+        // resulting consolidated policy is one that matches the default *device* settings.
+        AutomaticZenRule zenRule = new AutomaticZenRule("name",
+                null,
+                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                null,  // null policy
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+        String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+        // enable the rule
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_TRUE),
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+
+        // inspect the consolidated policy, which should match the device default settings.
+        assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy))
+                .isEqualTo(mZenModeHelper.getDefaultZenPolicy());
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_API)
+    public void testUpdateConsolidatedPolicy_preModesApiCustomPolicyOnly_fillInWithGlobal() {
         setupZenConfig();
 
         // when there's only one automatic rule active and it has a custom policy, make sure that's
-        // what the consolidated policy reflects whether or not it's stricter than what the default
-        // would specify.
+        // what the consolidated policy reflects whether or not it's stricter than what the global
+        // config would specify.
         ZenPolicy customPolicy = new ZenPolicy.Builder()
                 .allowAlarms(true)  // more lenient than default
                 .allowMedia(true)  // more lenient than default
@@ -3249,7 +3634,51 @@
     }
 
     @Test
-    public void testUpdateConsolidatedPolicy_defaultAndCustomActive() {
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDeviceDefault() {
+        setupZenConfig();
+
+        // when there's only one automatic rule active and it has a custom policy, make sure that's
+        // what the consolidated policy reflects whether or not it's stricter than what the default
+        // would specify.
+        ZenPolicy customPolicy = new ZenPolicy.Builder()
+                .allowSystem(true)  // more lenient than default
+                .allowRepeatCallers(false)  // more restrictive than default
+                .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE)  // more restrictive than default
+                .showFullScreenIntent(true)  // more lenient
+                .showBadges(false)  // more restrictive
+                .build();
+
+        AutomaticZenRule zenRule = new AutomaticZenRule("name",
+                null,
+                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                customPolicy,
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+        String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+        // enable the rule; this will update the consolidated policy
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_TRUE),
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+
+        // since this is the only active rule, the consolidated policy should match the custom
+        // policy for every field specified, and take default values for unspecified things
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue();  // custom
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse();  // custom
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()).isFalse();  // custom
+        assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse();  // custom
+        assertThat(mZenModeHelper.mConsolidatedPolicy.showFullScreenIntents()).isTrue();  // custom
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_API)
+    public void testUpdateConsolidatedPolicy_preModesApiDefaultAndCustomActive_mergesWithGlobal() {
         setupZenConfig();
 
         // when there are two rules active, one inheriting the default policy and one setting its
@@ -3309,6 +3738,68 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() {
+        setupZenConfig();
+
+        // when there are two rules active, one inheriting the default policy and one setting its
+        // own custom policy, they should be merged to form the most restrictive combination.
+
+        // rule 1: no custom policy
+        AutomaticZenRule zenRule = new AutomaticZenRule("name",
+                null,
+                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                null,
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+        String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+        // enable rule 1
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_TRUE),
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+
+        // custom policy for rule 2
+        ZenPolicy customPolicy = new ZenPolicy.Builder()
+                .allowAlarms(false) // more restrictive than default
+                .allowSystem(true)  // more lenient than default
+                .allowRepeatCallers(false)  // more restrictive than default
+                .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE)  // more restrictive than default
+                .showBadges(false)  // more restrictive
+                .showPeeking(true)  // more lenient
+                .build();
+
+        AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
+                null,
+                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                customPolicy,
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+        String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+        // enable rule 2; this will update the consolidated policy
+        mZenModeHelper.setAutomaticZenRuleState(id2,
+                new Condition(zenRule2.getConditionId(), "", STATE_TRUE),
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+
+        // now both rules should be on, and the consolidated policy should reflect the most
+        // restrictive option of each of the two
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse();  // custom stricter
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse();  // default stricter
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse();  // custom stricter
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers())
+                .isFalse();  // custom stricter
+        assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse();  // custom stricter
+        assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isFalse();  // default stricter
+    }
+
+    @Test
     public void testUpdateConsolidatedPolicy_allowChannels() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
         setupZenConfig();
@@ -3372,7 +3863,10 @@
                 null,
                 new ComponentName(CUSTOM_PKG_NAME, "cls"),
                 Uri.parse("priority"),
-                new ZenPolicy.Builder().allowMedia(true).build(),
+                new ZenPolicy.Builder()
+                        .allowMedia(true)
+                        .allowSystem(true)
+                        .build(),
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String rule1Id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
                 zenRuleWithPriority, UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID);
@@ -3394,10 +3888,10 @@
                 UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
 
         // Consolidated Policy should be default + rule1.
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue();  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // priority rule
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse();  // default
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isTrue();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue();  // priority rule
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse();  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isTrue();  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue();  // default
@@ -3408,7 +3902,7 @@
     public void zenRuleToAutomaticZenRule_allFields() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
-                new String[] {OWNER.getPackageName()});
+                new String[]{OWNER.getPackageName()});
 
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = CONFIG_ACTIVITY;
@@ -3452,7 +3946,7 @@
     public void automaticZenRuleToZenRule_allFields() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
-                new String[] {OWNER.getPackageName()});
+                new String[]{OWNER.getPackageName()});
 
         AutomaticZenRule azr = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
                 .setEnabled(true)
@@ -3478,7 +3972,8 @@
         assertEquals(CONDITION_ID, storedRule.conditionId);
         assertEquals(INTERRUPTION_FILTER_ZR, storedRule.zenMode);
         assertEquals(ENABLED, storedRule.enabled);
-        assertEquals(POLICY, storedRule.zenPolicy);
+        assertEquals(mZenModeHelper.getDefaultZenPolicy().overwrittenWith(POLICY),
+                storedRule.zenPolicy);
         assertEquals(CONFIG_ACTIVITY, storedRule.configurationActivity);
         assertEquals(TYPE, storedRule.type);
         assertEquals(ALLOW_MANUAL, storedRule.allowManualInvocation);
@@ -3561,12 +4056,12 @@
 
         // Modifies the zen policy and device effects
         ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
-                .allowPriorityChannels(true)
+                .allowPriorityChannels(false)
                 .build();
         ZenDeviceEffects deviceEffects =
                 new ZenDeviceEffects.Builder(rule.getDeviceEffects())
-                .setShouldDisplayGrayscale(true)
-                .build();
+                        .setShouldDisplayGrayscale(true)
+                        .build();
         AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
                 .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
                 .setZenPolicy(policy)
@@ -3580,7 +4075,8 @@
 
         // UPDATE_ORIGIN_USER should change the bitmask and change the values.
         assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
-        assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+
         assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
 
         ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
@@ -3770,9 +4266,9 @@
     @Test
     @EnableFlags(Flags.FLAG_MODES_API)
     public void updateAutomaticZenRule_nullPolicyUpdate() {
-        // Adds a starting rule with empty zen policies and device effects
+        // Adds a starting rule with set zen policy and empty device effects
         AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
-                .setZenPolicy(new ZenPolicy.Builder().build())
+                .setZenPolicy(POLICY)
                 .build();
         // Adds the rule using the app, to avoid having any user modified bits set.
         String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
@@ -3790,8 +4286,10 @@
                 Process.SYSTEM_UID);
         rule = mZenModeHelper.getAutomaticZenRule(ruleId);
 
-        // When AZR's ZenPolicy is null, we expect the updated rule's policy to be null.
-        assertThat(rule.getZenPolicy()).isNull();
+        // When AZR's ZenPolicy is null, we expect the updated rule's policy to be unchanged
+        // (equivalent to the provided policy, with additional fields filled in with defaults).
+        assertThat(rule.getZenPolicy()).isEqualTo(
+                mZenModeHelper.getDefaultZenPolicy().overwrittenWith(POLICY));
     }
 
     @Test
@@ -3847,13 +4345,13 @@
         assertThat(storedRule.canBeUpdatedByApp()).isFalse();
         assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(
                 ZenPolicy.FIELD_ALLOW_CHANNELS
-                | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
-                | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS
-                | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM
-                | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT
-                | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS
-                | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK
-                | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT
+                        | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
+                        | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS
+                        | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM
+                        | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT
+                        | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS
+                        | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK
+                        | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT
         );
     }
 
@@ -4016,6 +4514,7 @@
         final int[] actualStatus = new int[2];
         ZenModeHelper.Callback callback = new ZenModeHelper.Callback() {
             int i = 0;
+
             @Override
             void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) {
                 if (Objects.equals(createdId, id)) {
@@ -4056,6 +4555,7 @@
         final int[] actualStatus = new int[2];
         ZenModeHelper.Callback callback = new ZenModeHelper.Callback() {
             int i = 0;
+
             @Override
             void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) {
                 if (Objects.equals(createdId, id)) {
@@ -4202,6 +4702,7 @@
                         .build()),
                 eq(UPDATE_ORIGIN_APP));
     }
+
     @Test
     public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
@@ -4607,7 +5108,8 @@
                 .comparingElementsUsing(IGNORE_METADATA)
                 .containsExactly(
                         expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                                null, true));
+                                mZenModeHelper.mConfig.toZenPolicy(), // copy of global config
+                                true));
     }
 
     @Test
@@ -4626,7 +5128,9 @@
         assertThat(mZenModeHelper.mConfig.automaticRules.values())
                 .comparingElementsUsing(IGNORE_METADATA)
                 .containsExactly(
-                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true));
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS,
+                                mZenModeHelper.mConfig.toZenPolicy(), // copy of global config
+                                true));
     }
 
     @Test
@@ -4820,6 +5324,10 @@
         mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy);
         String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet());
 
+        // Store this for checking later.
+        ZenPolicy originalEffectiveZenPolicy = new ZenPolicy.Builder(
+                mZenModeHelper.mConfig.toZenPolicy()).allowMedia(true).build();
+
         // From user, update that rule's policy.
         AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
         ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds()
@@ -4839,7 +5347,9 @@
                 .comparingElementsUsing(IGNORE_METADATA)
                 .containsExactly(
                         expectedImplicitRule(pkg, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                                userUpdateZenPolicy,
+                                // the final policy for the rule should contain the user's update
+                                // overlaid on top of the original existing policy.
+                                originalEffectiveZenPolicy.overwrittenWith(userUpdateZenPolicy),
                                 /* conditionActive= */ null));
     }
 
@@ -4854,6 +5364,10 @@
         mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy);
         String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet());
 
+        // Store this for checking later.
+        ZenPolicy originalEffectiveZenPolicy = new ZenPolicy.Builder(
+                mZenModeHelper.mConfig.toZenPolicy()).allowMedia(true).build();
+
         // From user, update something in that rule, but not the ZenPolicy.
         AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
         AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
@@ -4873,7 +5387,7 @@
                 .allowPriorityChannels(true)
                 .build();
         assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenPolicy)
-                .isEqualTo(appsSecondZenPolicy);
+                .isEqualTo(originalEffectiveZenPolicy.overwrittenWith(appsSecondZenPolicy));
     }
 
     @Test
@@ -4905,14 +5419,18 @@
     }
 
     @Test
-    public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_returnsGlobalPolicy() {
+    public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_copiesGlobalPolicy() {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
-        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
-                ZEN_MODE_ALARMS);
         mZenModeHelper.mConfig.allowCalls = true;
         mZenModeHelper.mConfig.allowConversations = false;
 
+        // Implicit rule will get the global policy at the time of rule creation.
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+
+        // If the policy then changes afterwards, we should keep the snapshotted version.
+        mZenModeHelper.mConfig.allowCalls = false;
+
         Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
                 CUSTOM_PKG_NAME);
 
@@ -4952,7 +5470,7 @@
                     p.recycle();
                 }
             },
-            "Ignoring timestamp and userModifiedFields");
+              "Ignoring timestamp and userModifiedFields");
 
     private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
             @Nullable Boolean conditionActive) {
@@ -4962,7 +5480,7 @@
         if (conditionActive != null) {
             rule.condition = conditionActive
                     ? new Condition(rule.conditionId,
-                            mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE)
+                    mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE)
                     : new Condition(rule.conditionId,
                             mContext.getString(R.string.zen_mode_implicit_deactivated),
                             STATE_FALSE);
@@ -5030,8 +5548,35 @@
         assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber());
     }
 
+    private void checkDndProtoMatchesDefaultZenConfig(DNDPolicyProto dndProto) {
+        if (!Flags.modesApi()) {
+            checkDndProtoMatchesSetupZenConfig(dndProto);
+            return;
+        }
+
+        // When modes_api flag is on, the default zen config is the device defaults.
+        assertThat(dndProto.getAlarms().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getMedia().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getSystem().getNumber()).isEqualTo(STATE_DISALLOW);
+        assertThat(dndProto.getReminders().getNumber()).isEqualTo(STATE_DISALLOW);
+        assertThat(dndProto.getCalls().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getAllowCallsFrom().getNumber()).isEqualTo(PEOPLE_STARRED);
+        assertThat(dndProto.getMessages().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getAllowMessagesFrom().getNumber()).isEqualTo(PEOPLE_STARRED);
+        assertThat(dndProto.getEvents().getNumber()).isEqualTo(STATE_DISALLOW);
+        assertThat(dndProto.getRepeatCallers().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getFullscreen().getNumber()).isEqualTo(STATE_DISALLOW);
+        assertThat(dndProto.getLights().getNumber()).isEqualTo(STATE_DISALLOW);
+        assertThat(dndProto.getPeek().getNumber()).isEqualTo(STATE_DISALLOW);
+        assertThat(dndProto.getStatusBar().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getBadge().getNumber()).isEqualTo(STATE_ALLOW);
+        assertThat(dndProto.getAmbient().getNumber()).isEqualTo(STATE_DISALLOW);
+        assertThat(dndProto.getNotificationList().getNumber()).isEqualTo(STATE_ALLOW);
+    }
+
     private static void withoutWtfCrash(Runnable test) {
-        Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {});
+        Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {
+        });
         try {
             test.run();
         } finally {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index 4ed55df..57e1132 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -269,6 +269,78 @@
     }
 
     @Test
+    public void testZenPolicyOverwrite_allUnsetPolicies() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        ZenPolicy.Builder builder = new ZenPolicy.Builder();
+        ZenPolicy unset = builder.build();
+
+        builder.allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS);
+        builder.allowMedia(false);
+        builder.allowEvents(true);
+        builder.showFullScreenIntent(false);
+        builder.showInNotificationList(false);
+        ZenPolicy set = builder.build();
+
+        ZenPolicy overwritten = set.overwrittenWith(unset);
+        assertThat(overwritten).isEqualTo(set);
+
+        // should actually work the other way too.
+        ZenPolicy overwrittenWithSet = unset.overwrittenWith(set);
+        assertThat(overwrittenWithSet).isEqualTo(set);
+    }
+
+    @Test
+    public void testZenPolicyOverwrite_someOverlappingFields_takeNewPolicy() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        ZenPolicy p1 = new ZenPolicy.Builder()
+                .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
+                .allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED)
+                .allowMedia(false)
+                .showBadges(true)
+                .build();
+
+        ZenPolicy p2 = new ZenPolicy.Builder()
+                .allowRepeatCallers(false)
+                .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT)
+                .allowMessages(ZenPolicy.PEOPLE_TYPE_NONE)
+                .showBadges(false)
+                .showPeeking(true)
+                .build();
+
+        // when p1 is overwritten with p2, all values from p2 win regardless of strictness, and
+        // remaining fields take values from p1.
+        ZenPolicy p1OverwrittenWithP2 = p1.overwrittenWith(p2);
+        assertThat(p1OverwrittenWithP2.getPriorityCallSenders())
+                .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS);  // from p1
+        assertThat(p1OverwrittenWithP2.getPriorityMessageSenders())
+                .isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE);  // from p2
+        assertThat(p1OverwrittenWithP2.getPriorityCategoryRepeatCallers())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);  // from p2
+        assertThat(p1OverwrittenWithP2.getPriorityCategoryMedia())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);  // from p1
+        assertThat(p1OverwrittenWithP2.getVisualEffectBadge())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);  // from p2
+        assertThat(p1OverwrittenWithP2.getVisualEffectPeek())
+                .isEqualTo(ZenPolicy.STATE_ALLOW); // from p2
+
+        ZenPolicy p2OverwrittenWithP1 = p2.overwrittenWith(p1);
+        assertThat(p2OverwrittenWithP1.getPriorityCallSenders())
+                .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS);  // from p1
+        assertThat(p2OverwrittenWithP1.getPriorityMessageSenders())
+                .isEqualTo(ZenPolicy.PEOPLE_TYPE_STARRED);  // from p1
+        assertThat(p2OverwrittenWithP1.getPriorityCategoryRepeatCallers())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);  // from p2
+        assertThat(p2OverwrittenWithP1.getPriorityCategoryMedia())
+                .isEqualTo(ZenPolicy.STATE_DISALLOW);  // from p1
+        assertThat(p2OverwrittenWithP1.getVisualEffectBadge())
+                .isEqualTo(ZenPolicy.STATE_ALLOW);  // from p1
+        assertThat(p2OverwrittenWithP1.getVisualEffectPeek())
+                .isEqualTo(ZenPolicy.STATE_ALLOW); // from p2
+    }
+
+    @Test
     public void testZenPolicyMessagesInvalid() {
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index ba7b52e..2a89b02 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -1762,32 +1762,6 @@
         assertEquals(1, task.getChildCount());
     }
 
-    /**
-     * Test that an activity will not be destroyed if it is marked as non-destroyable.
-     */
-    @Test
-    public void testSafelyDestroy_nonDestroyable() {
-        final ActivityRecord activity = createActivityWithTask();
-        doReturn(false).when(activity).isDestroyable();
-
-        activity.safelyDestroy("test");
-
-        verify(activity, never()).destroyImmediately(anyString());
-    }
-
-    /**
-     * Test that an activity will not be destroyed if it is marked as non-destroyable.
-     */
-    @Test
-    public void testSafelyDestroy_destroyable() {
-        final ActivityRecord activity = createActivityWithTask();
-        doReturn(true).when(activity).isDestroyable();
-
-        activity.safelyDestroy("test");
-
-        verify(activity).destroyImmediately(anyString());
-    }
-
     @Test
     public void testRemoveImmediately() {
         final Consumer<Consumer<ActivityRecord>> test = setup -> {
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 1c57623..29faed1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -132,8 +132,10 @@
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -563,6 +565,86 @@
         return Pair.create(splitPrimaryActivity, splitSecondActivity);
     }
 
+    /**
+     * This test ensures that if the intent is being delivered to a desktop mode unfocused task
+     * while it is already on top, reports it as delivering to top.
+     */
+    @Test
+    public void testDesktopModeDeliverToTop() {
+        final ActivityStarter starter = prepareStarter(
+                FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_SINGLE_TOP,
+                false /* mockGetRootTask */);
+        final List<ActivityRecord> activities = createActivitiesInDesktopMode();
+
+        // Set focus back to the first task.
+        activities.get(0).moveFocusableActivityToTop("testDesktopModeDeliverToTop");
+
+        // Start activity and delivered new intent.
+        starter.getIntent().setComponent(activities.get(3).mActivityComponent);
+        doReturn(activities.get(3)).when(mRootWindowContainer).findTask(any(), any());
+        final int result = starter.setReason("testDesktopModeDeliverToTop").execute();
+
+        // Ensure result is delivering intent to top.
+        assertEquals(START_DELIVERED_TO_TOP, result);
+    }
+
+    /**
+     * This test ensures that if the intent is being delivered to a desktop mode unfocused task
+     * reports it is brought to front instead of delivering to top.
+     */
+    @Test
+    public void testDesktopModeTaskToFront() {
+        final ActivityStarter starter = prepareStarter(
+                FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_SINGLE_TOP, false);
+        final List<ActivityRecord> activities = createActivitiesInDesktopMode();
+        final ActivityRecord desktopModeFocusActivity = activities.get(0);
+        final ActivityRecord desktopModeReusableActivity = activities.get(1);
+        final ActivityRecord desktopModeTopActivity = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setParentTask(desktopModeReusableActivity.getRootTask()).build();
+        assertTrue(desktopModeTopActivity.inMultiWindowMode());
+
+        // Let first stack has focus.
+        desktopModeFocusActivity.moveFocusableActivityToTop("testDesktopModeTaskToFront");
+
+        // Start activity and delivered new intent.
+        starter.getIntent().setComponent(desktopModeReusableActivity.mActivityComponent);
+        doReturn(desktopModeReusableActivity).when(mRootWindowContainer).findTask(any(), any());
+        final int result = starter.setReason("testDesktopModeMoveToFront").execute();
+
+        // Ensure result is moving task to front.
+        assertEquals(START_TASK_TO_FRONT, result);
+    }
+
+    /** Returns 4 activities. */
+    private List<ActivityRecord> createActivitiesInDesktopMode() {
+        final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm);
+        List<ActivityRecord> activityRecords = new ArrayList<>();
+
+        for (int i = 0; i < 4; i++) {
+            Rect bounds = new Rect(desktopOrganizer.getDefaultDesktopTaskBounds());
+            bounds.offset(20 * i, 20 * i);
+            desktopOrganizer.createTask(bounds);
+        }
+
+        for (int i = 0; i < 4; i++) {
+            activityRecords.add(new TaskBuilder(mSupervisor)
+                    .setParentTask(desktopOrganizer.mTasks.get(i))
+                    .setCreateActivity(true)
+                    .build()
+                    .getTopMostActivity());
+        }
+
+        for (int i = 0; i < 4; i++) {
+            activityRecords.get(i).setVisibleRequested(true);
+        }
+
+        for (int i = 0; i < 4; i++) {
+            assertEquals(desktopOrganizer.mTasks.get(i), activityRecords.get(i).getRootTask());
+        }
+
+        return activityRecords;
+    }
+
     @Test
     public void testMoveVisibleTaskToFront() {
         final ActivityRecord activity = new TaskBuilder(mSupervisor)
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 402cbcc..c44be7b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -56,6 +56,7 @@
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.util.ArraySet;
 import android.view.WindowManager;
 import android.window.BackAnimationAdapter;
@@ -69,6 +70,7 @@
 import android.window.WindowOnBackInvokedDispatcher;
 
 import com.android.server.LocalServices;
+import com.android.window.flags.Flags;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -81,6 +83,12 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
+/**
+ * Tests for the {@link BackNavigationController} class.
+ *
+ * Build/Install/Run:
+ *  atest WmTests:BackNavigationControllerTests
+ */
 @Presubmit
 @RunWith(WindowTestRunner.class)
 public class BackNavigationControllerTests extends WindowTestsBase {
@@ -623,6 +631,22 @@
                 0, navigationObserver.getCount());
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG)
+    public void testAdjacentFocusInActivityEmbedding() {
+        Task task = createTask(mDefaultDisplay);
+        TaskFragment primary = createTaskFragmentWithActivity(task);
+        TaskFragment secondary = createTaskFragmentWithActivity(task);
+        primary.setAdjacentTaskFragment(secondary);
+        secondary.setAdjacentTaskFragment(primary);
+
+        WindowState windowState = mock(WindowState.class);
+        doReturn(windowState).when(mWm).getFocusedWindowLocked();
+        doReturn(primary).when(windowState).getTaskFragment();
+
+        startBackNavigation();
+        verify(mWm).moveFocusToAdjacentWindow(any(), anyInt());
+    }
 
     /**
      * Test with
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 782d89c..95850ac 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2131,8 +2131,8 @@
         // Once transition starts, rotation is applied and transition shows DC rotating.
         testPlayer.startTransition();
         waitUntilHandlersIdle();
-        verify(activity1).ensureActivityConfiguration(anyBoolean(), anyBoolean());
-        verify(activity2).ensureActivityConfiguration(anyBoolean(), anyBoolean());
+        verify(activity1).ensureActivityConfiguration(anyBoolean());
+        verify(activity2).ensureActivityConfiguration(anyBoolean());
         assertNotEquals(origRot, dc.getConfiguration().windowConfiguration.getRotation());
         assertNotNull(testPlayer.mLastReady);
         assertTrue(testPlayer.mController.isPlaying());
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index be96e60..9e00f92 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -283,12 +283,12 @@
 
         policy.screenTurnedOff();
         policy.setAwake(false);
-        policy.screenTurnedOn(null /* screenOnListener */);
+        policy.screenTurningOn(null /* screenOnListener */);
         assertTrue(wpc.isShowingUiWhileDozing());
         policy.screenTurnedOff();
         assertFalse(wpc.isShowingUiWhileDozing());
 
-        policy.screenTurnedOn(null /* screenOnListener */);
+        policy.screenTurningOn(null /* screenOnListener */);
         assertTrue(wpc.isShowingUiWhileDozing());
         policy.setAwake(true);
         assertFalse(wpc.isShowingUiWhileDozing());
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 5518c60..6759eef 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -197,10 +197,10 @@
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         // Translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
 
         translucentActivity.setState(DESTROYED, "testing");
@@ -225,10 +225,10 @@
 
         // Translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
 
         spyOn(translucentActivity.mLetterboxUiController);
@@ -300,10 +300,10 @@
 
         // Translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
 
         spyOn(translucentActivity.mLetterboxUiController);
@@ -376,10 +376,10 @@
 
         // Launch translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
         // Transparent strategy applied
         assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
@@ -404,10 +404,10 @@
 
         // Launch translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
         // Transparent strategy applied
         assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
@@ -441,10 +441,10 @@
 
         // Launch translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
         // Transparent strategy applied
         assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
@@ -465,12 +465,12 @@
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         // Translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
                 .setMinAspectRatio(1.1f)
                 .setMaxAspectRatio(3f)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
         // We check bounds
         final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
@@ -493,9 +493,9 @@
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         // Translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         final Configuration requestedConfig =
                 translucentActivity.getRequestedOverrideConfiguration();
         final WindowConfiguration translucentWinConf = requestedConfig.windowConfiguration;
@@ -525,12 +525,12 @@
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         // Translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
                 .setMinAspectRatio(1.1f)
                 .setMaxAspectRatio(3f)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
         // We check bounds
         final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
@@ -538,10 +538,10 @@
         assertEquals(opaqueBounds, translucentRequestedBounds);
         // Launch another translucent activity
         final ActivityRecord translucentActivity2 = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
                 .build();
-        doReturn(false).when(translucentActivity2).fillsParent();
         mTask.addChild(translucentActivity2);
         // We check bounds
         final Rect translucent2RequestedBounds = translucentActivity2.getRequestedOverrideBounds();
@@ -558,9 +558,9 @@
         // simplicity.
         doReturn(true).when(mActivity).isEmbedded();
         // Translucent Activity
-        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm).build();
+        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent).build();
         doReturn(false).when(translucentActivity).matchParentBounds();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
         // Check the strategy has not being applied
         assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
@@ -580,10 +580,10 @@
         assertFalse(mActivity.inSizeCompatMode());
         // We launch a transparent activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
         // It should not be in SCM
         assertFalse(translucentActivity.inSizeCompatMode());
@@ -600,12 +600,16 @@
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         // Translucent Activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
-        spyOn(mActivity);
+        assertFalse(translucentActivity.fillsParent());
+        assertTrue(mActivity.fillsParent());
+        mActivity.finishing = true;
+        assertFalse(mActivity.occludesParent());
         mTask.addChild(translucentActivity);
-        verify(mActivity).isFinishing();
+        // The translucent activity won't inherit letterbox behavior from a finishing activity.
+        assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
     }
 
     @Test
@@ -619,10 +623,10 @@
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
         // We launch a transparent activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
         assertEquals(translucentActivity.getBounds(), mActivity.getBounds());
 
@@ -655,10 +659,10 @@
 
         // We launch a transparent activity
         final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setActivityTheme(android.R.style.Theme_Translucent)
                 .setLaunchedFromUid(mActivity.getUid())
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .build();
-        doReturn(false).when(translucentActivity).fillsParent();
         mTask.addChild(translucentActivity);
 
         // The transparent activity inherits the compat display insets of the opaque activity
@@ -1020,8 +1024,17 @@
         // Activity is sandboxed due to fixed aspect ratio.
         assertActivityMaxBoundsSandboxed();
 
+        // Prepare the states for verifying relaunching after changing orientation.
+        mActivity.finishRelaunching();
+        mActivity.setState(RESUMED, "testFixedAspectRatioOrientationChangeOrientation");
+        mActivity.setLastReportedConfiguration(mAtm.getGlobalConfiguration(),
+                mActivity.getConfiguration());
+
         // Change the fixed orientation.
         mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+        assertTrue(mActivity.isRelaunching());
+        assertTrue(mActivity.mLetterboxUiController
+                .getIsRelaunchingAfterRequestedOrientationChanged());
 
         assertFitted();
         assertEquals(originalBounds.width(), mActivity.getBounds().height());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index 45ecc3f..00ecd00 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -215,7 +215,7 @@
                 doReturn(false).when(newDisplay).supportsSystemDecorations();
             }
             // Update the display policy to make the screen fully turned on so animation is allowed
-            displayPolicy.screenTurnedOn(null /* screenOnListener */);
+            displayPolicy.screenTurningOn(null /* screenOnListener */);
             displayPolicy.finishKeyguardDrawn();
             displayPolicy.finishWindowsDrawn();
             displayPolicy.finishScreenTurningOn();
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 114b9c3..a0bafb6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -133,7 +134,9 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /** Common base class for window manager unit test classes. */
 class WindowTestsBase extends SystemServiceTestsBase {
@@ -226,7 +229,7 @@
         mDefaultDisplay = mWm.mRoot.getDefaultDisplay();
         // Update the display policy to make the screen fully turned on so animation is allowed
         final DisplayPolicy displayPolicy = mDefaultDisplay.getDisplayPolicy();
-        displayPolicy.screenTurnedOn(null /* screenOnListener */);
+        displayPolicy.screenTurningOn(null /* screenOnListener */);
         displayPolicy.finishKeyguardDrawn();
         displayPolicy.finishWindowsDrawn();
         displayPolicy.finishScreenTurningOn();
@@ -1892,6 +1895,55 @@
         }
     }
 
+    static class TestDesktopOrganizer extends WindowOrganizerTests.StubOrganizer {
+        final int mDesktopModeDefaultWidthDp = 840;
+        final int mDesktopModeDefaultHeightDp = 630;
+        final int mDesktopDensity = 284;
+
+        final ActivityTaskManagerService mService;
+        final TaskDisplayArea mDefaultTDA;
+        List<Task> mTasks;
+        final DisplayContent mDisplay;
+        Rect mStableBounds;
+
+        TestDesktopOrganizer(ActivityTaskManagerService service, DisplayContent display) {
+            mService = service;
+            mDefaultTDA = display.getDefaultTaskDisplayArea();
+            mDisplay = display;
+            mService.mTaskOrganizerController.registerTaskOrganizer(this);
+            mTasks = new ArrayList<>();
+            mStableBounds = display.getBounds();
+        }
+
+        TestDesktopOrganizer(ActivityTaskManagerService service) {
+            this(service, service.mTaskSupervisor.mRootWindowContainer.getDefaultDisplay());
+        }
+
+        public Task createTask(Rect bounds) {
+            Task task = mService.mTaskOrganizerController.createRootTask(
+                    mDisplay, WINDOWING_MODE_FREEFORM, null);
+            task.setBounds(bounds);
+            mTasks.add(task);
+            spyOn(task);
+            return task;
+        }
+
+        public Rect getDefaultDesktopTaskBounds() {
+            int width = (int) (mDesktopModeDefaultWidthDp * mDesktopDensity + 0.5f);
+            int height = (int) (mDesktopModeDefaultHeightDp * mDesktopDensity + 0.5f);
+            Rect outBounds = new Rect();
+
+            outBounds.set(0, 0, width, height);
+            // Center the task in stable bounds
+            outBounds.offset(
+                    mStableBounds.centerX() - outBounds.centerX(),
+                    mStableBounds.centerY() - outBounds.centerY()
+            );
+            return outBounds;
+        }
+
+    }
+
     static TestWindowToken createTestWindowToken(int type, DisplayContent dc) {
         return createTestWindowToken(type, dc, false /* persistOnEmpty */);
     }
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index 94c737d..a089f5c 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.MODIFY_PHONE_STATE;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
@@ -30,11 +31,15 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.util.ArraySet;
+
+import com.android.internal.telephony.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * Represents a distinct method to place or receive a phone call. Apps which can place calls and
@@ -491,6 +496,7 @@
     private final Bundle mExtras;
     private boolean mIsEnabled;
     private String mGroupId;
+    private final Set<PhoneAccountHandle> mSimultaneousCallingRestriction;
 
     @Override
     public boolean equals(Object o) {
@@ -508,7 +514,9 @@
                 Objects.equals(mShortDescription, that.mShortDescription) &&
                 Objects.equals(mSupportedUriSchemes, that.mSupportedUriSchemes) &&
                 areBundlesEqual(mExtras, that.mExtras) &&
-                Objects.equals(mGroupId, that.mGroupId);
+                Objects.equals(mGroupId, that.mGroupId)
+                && Objects.equals(mSimultaneousCallingRestriction,
+                        that.mSimultaneousCallingRestriction);
     }
 
     @Override
@@ -516,7 +524,7 @@
         return Objects.hash(mAccountHandle, mAddress, mSubscriptionAddress, mCapabilities,
                 mHighlightColor, mLabel, mShortDescription, mSupportedUriSchemes,
                 mSupportedAudioRoutes,
-                mExtras, mIsEnabled, mGroupId);
+                mExtras, mIsEnabled, mGroupId, mSimultaneousCallingRestriction);
     }
 
     /**
@@ -537,6 +545,7 @@
         private Bundle mExtras;
         private boolean mIsEnabled = false;
         private String mGroupId = "";
+        private Set<PhoneAccountHandle> mSimultaneousCallingRestriction = null;
 
         /**
          * Creates a builder with the specified {@link PhoneAccountHandle} and label.
@@ -787,6 +796,57 @@
         }
 
         /**
+         * Restricts the ability of this {@link PhoneAccount} to ONLY support simultaneous calling
+         * with the other {@link PhoneAccountHandle}s in this Set.
+         * <p>
+         * If two or more {@link PhoneAccount}s support calling simultaneously, it means that
+         * Telecom allows the user to place additional outgoing calls and receive additional
+         * incoming calls using other {@link PhoneAccount}s while this PhoneAccount also has one or
+         * more active calls.
+         * <p>
+         * If this setter method is never called or cleared using
+         * {@link #clearSimultaneousCallingRestriction()}, there is no restriction and all
+         * {@link PhoneAccount}s registered to Telecom by this package support simultaneous calling.
+         * <p>
+         * Note: Simultaneous calling restrictions can only be placed on {@link PhoneAccount}s that
+         * were registered by the same application. Simultaneous calling across applications is
+         * always possible as long as the {@link Connection} supports hold. If a
+         * {@link PhoneAccountHandle} is included here and the package name doesn't match this
+         * application's package name, {@link TelecomManager#registerPhoneAccount(PhoneAccount)}
+         * will throw a {@link SecurityException}.
+         *
+         * @param handles The other {@link PhoneAccountHandle}s that support calling simultaneously
+         * with this one. Use {@link #clearSimultaneousCallingRestriction()} to remove the
+         * restriction and allow simultaneous calling to be supported across all
+         * {@link PhoneAccount}s registered by this package.
+         * @return The Builder used to set up the new PhoneAccount.
+         */
+        @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS)
+        public @NonNull Builder setSimultaneousCallingRestriction(
+                @NonNull Set<PhoneAccountHandle> handles) {
+            if (handles == null) {
+                throw new IllegalArgumentException("the Set of PhoneAccountHandles must not be "
+                        + "null");
+            }
+            mSimultaneousCallingRestriction = handles;
+            return this;
+        }
+
+        /**
+         * Clears a previously set simultaneous calling restriction set when
+         * {@link PhoneAccount.Builder#Builder(PhoneAccount)} is used to create a new PhoneAccount
+         * from an existing one.
+         *
+         * @return The Builder used to set up the new PhoneAccount.
+         * @see #setSimultaneousCallingRestriction(Set)
+         */
+        @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS)
+        public @NonNull Builder clearSimultaneousCallingRestriction() {
+            mSimultaneousCallingRestriction = null;
+            return this;
+        }
+
+        /**
          * Creates an instance of a {@link PhoneAccount} based on the current builder settings.
          *
          * @return The {@link PhoneAccount}.
@@ -810,7 +870,8 @@
                     mExtras,
                     mSupportedAudioRoutes,
                     mIsEnabled,
-                    mGroupId);
+                    mGroupId,
+                    mSimultaneousCallingRestriction);
         }
     }
 
@@ -827,7 +888,8 @@
             Bundle extras,
             int supportedAudioRoutes,
             boolean isEnabled,
-            String groupId) {
+            String groupId,
+            Set<PhoneAccountHandle> simultaneousCallingRestriction) {
         mAccountHandle = account;
         mAddress = address;
         mSubscriptionAddress = subscriptionAddress;
@@ -841,6 +903,7 @@
         mSupportedAudioRoutes = supportedAudioRoutes;
         mIsEnabled = isEnabled;
         mGroupId = groupId;
+        mSimultaneousCallingRestriction = simultaneousCallingRestriction;
     }
 
     public static Builder builder(
@@ -1050,6 +1113,49 @@
         return (mCapabilities & CAPABILITY_SELF_MANAGED) == CAPABILITY_SELF_MANAGED;
     }
 
+    /**
+     * If a restriction is set (see {@link #hasSimultaneousCallingRestriction()}), this method
+     * returns the Set of {@link PhoneAccountHandle}s that are allowed to support calls
+     * simultaneously with this {@link PhoneAccount}.
+     * <p>
+     * If this {@link PhoneAccount} is busy with one or more ongoing calls, a restriction is set on
+     * this PhoneAccount (see {@link #hasSimultaneousCallingRestriction()} to check),  and a new
+     * incoming or outgoing call is received or placed on a PhoneAccount that is not in this Set,
+     * Telecom will reject or cancel the pending call in favor of keeping the ongoing call alive.
+     * <p>
+     * Note: Simultaneous calling restrictions can only be placed on {@link PhoneAccount}s that
+     * were registered by the same application. Simultaneous calling across applications is
+     * always possible as long as the {@link Connection} supports hold.
+     *
+     * @return the Set of {@link PhoneAccountHandle}s that this {@link PhoneAccount} supports
+     * simultaneous calls with.
+     * @throws IllegalStateException If there is no restriction set on this {@link PhoneAccount}
+     * and this method is called. Whether or not there is a restriction can be checked using
+     * {@link #hasSimultaneousCallingRestriction()}.
+     */
+    @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS)
+    public @NonNull Set<PhoneAccountHandle> getSimultaneousCallingRestriction() {
+        if (mSimultaneousCallingRestriction == null) {
+            throw new IllegalStateException("This method can not be called if there is no "
+                    + "simultaneous calling restriction. See #hasSimultaneousCallingRestriction");
+        }
+        return mSimultaneousCallingRestriction;
+    }
+
+    /**
+     * Whether or not this {@link PhoneAccount} contains a simultaneous calling restriction on it.
+     *
+     * @return {@code true} if this PhoneAccount contains a simultaneous calling restriction,
+     * {@code false} if it does not. Use {@link #getSimultaneousCallingRestriction()} to query which
+     * other {@link PhoneAccount}s support simultaneous calling with this one.
+     * @see #getSimultaneousCallingRestriction() for more information on how the sinultaneous
+     * calling restriction works.
+     */
+    @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS)
+    public boolean hasSimultaneousCallingRestriction() {
+        return mSimultaneousCallingRestriction != null;
+    }
+
     //
     // Parcelable implementation
     //
@@ -1095,6 +1201,12 @@
         out.writeBundle(mExtras);
         out.writeString(mGroupId);
         out.writeInt(mSupportedAudioRoutes);
+        if (mSimultaneousCallingRestriction == null) {
+            out.writeBoolean(false);
+        } else {
+            out.writeBoolean(true);
+            out.writeTypedList(mSimultaneousCallingRestriction.stream().toList());
+        }
     }
 
     public static final @android.annotation.NonNull Creator<PhoneAccount> CREATOR
@@ -1140,6 +1252,13 @@
         mExtras = in.readBundle();
         mGroupId = in.readString();
         mSupportedAudioRoutes = in.readInt();
+        if (in.readBoolean()) {
+            List<PhoneAccountHandle> list = new ArrayList<>();
+            in.readTypedList(list, PhoneAccountHandle.CREATOR);
+            mSimultaneousCallingRestriction = new ArraySet<>(list);
+        } else {
+            mSimultaneousCallingRestriction = null;
+        }
     }
 
     @Override
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index a5c6d57..1bf11df 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1618,14 +1618,15 @@
     }
 
     /**
-     * Register for changes to the list of active {@link SubscriptionInfo} records or to the
-     * individual records themselves. When a change occurs the onSubscriptionsChanged method of
-     * the listener will be invoked immediately if there has been a notification. The
-     * onSubscriptionChanged method will also be triggered once initially when calling this
-     * function.
+     * Register for changes to the list of {@link SubscriptionInfo} records or to the
+     * individual records (active or inactive) themselves. When a change occurs, the
+     * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method of
+     * the listener will be invoked immediately. The
+     * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method will also be invoked
+     * once initially when calling this method.
      *
      * @param listener an instance of {@link OnSubscriptionsChangedListener} with
-     *                 onSubscriptionsChanged overridden.
+     * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} overridden.
      * @param executor the executor that will execute callbacks.
      */
     public void addOnSubscriptionsChangedListener(
@@ -1953,7 +1954,6 @@
      *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
-    // @RequiresPermission(TODO(b/308809058))
     public List<SubscriptionInfo> getActiveSubscriptionInfoList() {
         List<SubscriptionInfo> activeList = null;
 
@@ -2010,6 +2010,9 @@
      * Create a new subscription manager instance that can see all subscriptions across
      * user profiles.
      *
+     * The permission check for accessing all subscriptions will be enforced upon calling the
+     * individual APIs linked below.
+     *
      * @return a SubscriptionManager that can see all subscriptions regardless its user profile
      * association.
      *
@@ -2018,9 +2021,7 @@
      * @see UserHandle
      */
     @FlaggedApi(Flags.FLAG_ENFORCE_SUBSCRIPTION_USER_FILTER)
-    // @RequiresPermission(TODO(b/308809058))
-    // The permission check for accessing all subscriptions will be enforced upon calling the
-    // individual APIs linked above.
+    @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES)
     @NonNull public SubscriptionManager createForAllUserProfiles() {
         return new SubscriptionManager(mContext, true/*isForAllUserProfiles*/);
     }
@@ -2215,7 +2216,6 @@
      *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
-    // @RequiresPermission(TODO(b/308809058))
     public int getActiveSubscriptionInfoCount() {
         int result = 0;
 
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 9ec5f7a..cbd5524 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6888,6 +6888,7 @@
         }
     }
 
+    // TODO(b/316183370): replace all @code with @link in javadoc after feature is released
     /**
      * @return true if the current device is "voice capable".
      * <p>
@@ -6901,7 +6902,10 @@
      * PackageManager.FEATURE_TELEPHONY system feature, which is available
      * on any device with a telephony radio, even if the device is
      * data-only.
-     * @deprecated Replaced by {@link #isDeviceVoiceCapable()}
+     * @deprecated Replaced by {@code #isDeviceVoiceCapable()}. Starting from Android 15, voice
+     * capability may also be overridden by carriers for a given subscription. For voice capable
+     * device (when {@code #isDeviceVoiceCapable} return {@code true}), caller should check for
+     * subscription-level voice capability as well. See {@code #isDeviceVoiceCapable} for details.
      */
     @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING)
     @Deprecated
@@ -6923,9 +6927,10 @@
      * .FEATURE_TELEPHONY system feature, which is available on any device with a telephony
      * radio, even if the device is data-only.
      * <p>
-     * To check if a subscription is "voice capable", call method
-     * {@link SubscriptionInfo#getServiceCapabilities()} and compare  the result with
-     * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_VOICE}.
+     * Starting from Android 15, voice capability may also be overridden by carrier for a given
+     * subscription on a voice capable device. To check if a subscription is "voice capable",
+     * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if
+     * {@code SubscriptionManager#SERVICE_CAPABILITY_VOICE} is included.
      *
      * @see SubscriptionInfo#getServiceCapabilities()
      */
@@ -6943,7 +6948,10 @@
      * <p>
      * Note: Voicemail waiting sms, cell broadcasting sms, and MMS are
      *       disabled when device doesn't support sms.
-     * @deprecated Replaced by {@link #isDeviceSmsCapable()}
+     * @deprecated Replaced by {@code #isDeviceSmsCapable()}. Starting from Android 15, SMS
+     * capability may also be overridden by carriers for a given subscription. For SMS capable
+     * device (when {@code #isDeviceSmsCapable} return {@code true}), caller should check for
+     * subscription-level SMS capability as well. See {@code #isDeviceSmsCapable} for details.
      */
     @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
     public boolean isSmsCapable() {
@@ -6961,9 +6969,10 @@
      * Note: Voicemail waiting SMS, cell broadcasting SMS, and MMS are
      *       disabled when device doesn't support SMS.
      * <p>
-     * To check if a subscription is "SMS capable", call method
-     * {@link SubscriptionInfo#getServiceCapabilities()} and compare result with
-     * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_SMS}.
+     * Starting from Android 15, SMS capability may also be overridden by carriers for a given
+     * subscription on an SMS capable device. To check if a subscription is "SMS capable",
+     * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if
+     * {@code SubscriptionManager#SERVICE_CAPABILITY_SMS} is included.
      *
      * @see SubscriptionInfo#getServiceCapabilities()
      */
@@ -13139,7 +13148,7 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     public ServiceState getServiceStateForSubscriber(int subId) {
-        return getServiceStateForSubscriber(getSubId(), false, false);
+        return getServiceStateForSubscriber(subId, false, false);
     }
 
     /**
@@ -18483,7 +18492,6 @@
 
     /**
      * Get last known cell identity.
-     * Require appropriate permissions, otherwise throws SecurityException.
      *
      * If there is current registered network this value will be same as the registered cell
      * identity. If the device goes out of service the previous cell identity is cached and
@@ -18495,7 +18503,7 @@
     @SystemApi
     @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
     @RequiresPermission(allOf = {Manifest.permission.ACCESS_FINE_LOCATION,
-            "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"})
+            Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID})
     public @Nullable CellIdentity getLastKnownCellIdentity() {
         try {
             ITelephony telephony = getITelephony();
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index acbf354..24296f4 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3078,6 +3078,16 @@
             in List<String> satelliteCountryCodes);
 
     /**
+     * This API can be used in only testing to override oem-enabled satellite provision status.
+     *
+     * @param reset {@code true} mean the overriding status should not be used, {@code false}
+     *              otherwise.
+     * @param isProvisioned The overriding provision status.
+     * @return {@code true} if the provision status is set successfully, {@code false} otherwise.
+     */
+    boolean setOemEnabledSatelliteProvisionStatus(in boolean reset, in boolean isProvisioned);
+
+    /**
      * Test method to confirm the file contents are not altered.
      */
      @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
diff --git a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
index 2bc056e..cee27b6 100644
--- a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
+++ b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
@@ -16,6 +16,8 @@
 
 package android.transparency.test.app;
 
+import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES;
+
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -27,6 +29,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.internal.os.IBinaryTransparencyService.AppInfo;
 
 import org.junit.Before;
@@ -116,7 +119,12 @@
     @Test
     public void testCollectAllSilentInstalledMbaInfo() {
         // Action
-        var appInfoList = mBt.collectAllSilentInstalledMbaInfo(new Bundle());
+        var appInfoList =
+                ShellIdentityUtils.invokeMethodWithShellPermissions(
+                        mBt,
+                        (Bt) ->
+                                mBt.collectAllSilentInstalledMbaInfo(new Bundle()),
+                        GET_BACKGROUND_INSTALLED_PACKAGES);
 
         // Verify
         assertThat(appInfoList).isNotEmpty();  // because we just installed from the host side
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index b058631..256a469 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -292,14 +292,16 @@
         setVirtualMousePointerDisplayIdAndVerify(10)
 
         localService.setPointerIconVisible(false, 10)
+        verify(native).setPointerIconVisibility(10, false)
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
         localService.setMousePointerAccelerationEnabled(false, 10)
-        verify(native).setMousePointerAccelerationEnabled(eq(false))
+        verify(native).setMousePointerAccelerationEnabled(10, false)
 
         service.onDisplayRemoved(10)
+        verify(native).setPointerIconVisibility(10, true)
         verify(native).displayRemoved(eq(10))
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
-        verify(native).setMousePointerAccelerationEnabled(true)
+        verify(native).setMousePointerAccelerationEnabled(10, true)
         verifyNoMoreInteractions(native)
 
         // This call should not block because the virtual mouse pointer override was never removed.
@@ -315,25 +317,26 @@
 
         localService.setPointerIconVisible(false, 10)
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
+        verify(native).setPointerIconVisibility(10, false)
         localService.setMousePointerAccelerationEnabled(false, 10)
-        verify(native).setMousePointerAccelerationEnabled(eq(false))
+        verify(native).setMousePointerAccelerationEnabled(10, false)
 
         localService.setPointerIconVisible(true, 10)
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
+        verify(native).setPointerIconVisibility(10, true)
         localService.setMousePointerAccelerationEnabled(true, 10)
-        verify(native).setMousePointerAccelerationEnabled(eq(true))
+        verify(native).setMousePointerAccelerationEnabled(10, true)
 
-        // Verify that setting properties on a different display is not propagated until the
-        // pointer is moved to that display.
         localService.setPointerIconVisible(false, 20)
+        verify(native).setPointerIconVisibility(20, false)
         localService.setMousePointerAccelerationEnabled(false, 20)
+        verify(native).setMousePointerAccelerationEnabled(20, false)
         verifyNoMoreInteractions(native)
 
         clearInvocations(native)
         setVirtualMousePointerDisplayIdAndVerify(20)
 
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-        verify(native).setMousePointerAccelerationEnabled(eq(false))
     }
 
     @Test
@@ -341,12 +344,13 @@
         localService.setPointerIconVisible(false, 10)
         localService.setMousePointerAccelerationEnabled(false, 10)
 
+        verify(native).setPointerIconVisibility(10, false)
+        verify(native).setMousePointerAccelerationEnabled(10, false)
         verifyNoMoreInteractions(native)
 
         setVirtualMousePointerDisplayIdAndVerify(10)
 
         verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-        verify(native).setMousePointerAccelerationEnabled(eq(false))
     }
 
     @Test
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
index cbdcb88..518183f 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.content.rollback.RollbackInfo;
 import android.content.rollback.RollbackManager;
 import android.os.UserManager;
@@ -146,7 +147,8 @@
             assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
 
             // Upgrade from v1 to v2, with rollbacks enabled.
-            Install.single(TestApp.A2).setEnableRollback().commit();
+            Install.single(TestApp.A2).setEnableRollback().setRollbackImpactLevel(
+                    PackageManager.ROLLBACK_USER_IMPACT_HIGH).commit();
             assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
 
             // The app should now be available for rollback.
@@ -154,6 +156,8 @@
             assertThat(available).isNotStaged();
             assertThat(available).packagesContainsExactly(
                     Rollback.from(TestApp.A2).to(TestApp.A1));
+            assertThat(available.getRollbackImpactLevel()).isEqualTo(
+                    PackageManager.ROLLBACK_USER_IMPACT_HIGH);
 
             // We should not have received any rollback requests yet.
             // TODO: Possibly flaky if, by chance, some other app on device
@@ -264,6 +268,8 @@
             RollbackInfo rollbackB = waitForAvailableRollback(TestApp.B);
             assertThat(rollbackB).packagesContainsExactly(
                     Rollback.from(TestApp.B2).to(TestApp.B1));
+            assertThat(rollbackB.getRollbackImpactLevel()).isEqualTo(
+                    PackageManager.ROLLBACK_USER_IMPACT_LOW);
 
             // Register rollback committed receiver
             RollbackBroadcastReceiver rollbackReceiver = new RollbackBroadcastReceiver();