Merge "Delete communal test factories in favor of kosmos" into main
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 4e61b0a..3bddfe1 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";
@@ -17875,6 +17876,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 +22155,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 +23319,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";
@@ -42218,9 +42223,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 +42268,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>);
   }
@@ -46947,6 +46956,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/test-current.txt b/core/api/test-current.txt
index a866a34..5d271cc 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
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/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/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/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/service/quickaccesswallet/QuickAccessWalletServiceInfo.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
index ce38bb8..e6d8fd0 100644
--- a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
+++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
@@ -19,6 +19,7 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.role.RoleManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -30,6 +31,7 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Drawable;
+import android.os.Binder;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -69,12 +71,17 @@
 
     @Nullable
     static QuickAccessWalletServiceInfo tryCreate(@NonNull Context context) {
-        ComponentName defaultPaymentApp = getDefaultPaymentApp(context);
-        if (defaultPaymentApp == null) {
-            return null;
+        String defaultAppPackageName = getDefaultWalletApp(context);
+
+        if (defaultAppPackageName == null) {
+            ComponentName defaultPaymentApp = getDefaultPaymentApp(context);
+            if (defaultPaymentApp == null) {
+                return null;
+            }
+            defaultAppPackageName = defaultPaymentApp.getPackageName();
         }
 
-        ServiceInfo serviceInfo = getWalletServiceInfo(context, defaultPaymentApp.getPackageName());
+        ServiceInfo serviceInfo = getWalletServiceInfo(context, defaultAppPackageName);
         if (serviceInfo == null) {
             return null;
         }
@@ -92,6 +99,20 @@
         return new QuickAccessWalletServiceInfo(serviceInfo, metadata, tileServiceMetadata);
     }
 
+    private static String getDefaultWalletApp(Context context) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            RoleManager roleManager = context.getSystemService(RoleManager.class);
+            if (roleManager.isRoleAvailable(RoleManager.ROLE_WALLET)) {
+                List<String> roleHolders = roleManager.getRoleHolders(RoleManager.ROLE_WALLET);
+                return roleHolders.isEmpty() ? null : roleHolders.get(0);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        return null;
+    }
+
     private static ComponentName getDefaultPaymentApp(Context context) {
         ContentResolver cr = context.getContentResolver();
         String comp = Settings.Secure.getString(cr, Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT);
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/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 a3d5cf6..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.
@@ -5695,6 +5702,7 @@
          @hide -->
     <permission android:name="android.permission.MANAGE_ROLE_HOLDERS"
                 android:protectionLevel="signature|installer|module" />
+    <uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />
 
     <!-- @SystemApi Allows an application to manage the holders of roles associated with default
          applications.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5be29a6..9e14006 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
@@ -5334,20 +5336,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
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d12ef2b..ef12d8f 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" />
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/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/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/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/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/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/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..dc2a625 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);
   }
 
 }
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/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/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/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/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/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..1eaa060 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
@@ -58,7 +58,9 @@
     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() {
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/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index be523b8..d7a7941 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
@@ -52,7 +52,7 @@
 
     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() }
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/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..c630a7f 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>
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/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/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index 3a247c5..3b8fb26 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -20,6 +20,7 @@
 import static android.provider.Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT;
 
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
+import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE;
 import static com.android.systemui.wallet.util.WalletCardUtilsKt.getPaymentCards;
 
 import android.content.Intent;
@@ -42,7 +43,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.MetricsLogger;
-import com.android.systemui.res.R;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -54,6 +54,7 @@
 import com.android.systemui.qs.QsEventLogger;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.wallet.controller.QuickAccessWalletController;
@@ -118,7 +119,8 @@
     protected void handleSetListening(boolean listening) {
         super.handleSetListening(listening);
         if (listening) {
-            mController.setupWalletChangeObservers(mCardRetriever, DEFAULT_PAYMENT_APP_CHANGE);
+            mController.setupWalletChangeObservers(mCardRetriever, DEFAULT_PAYMENT_APP_CHANGE,
+                    DEFAULT_WALLET_APP_CHANGE);
             if (!mController.getWalletClient().isWalletServiceAvailable()
                     || !mController.getWalletClient().isWalletFeatureAvailable()) {
                 Log.i(TAG, "QAW service is unavailable, recreating the wallet client.");
@@ -201,7 +203,8 @@
     @Override
     protected void handleDestroy() {
         super.handleDestroy();
-        mController.unregisterWalletChangeObservers(DEFAULT_PAYMENT_APP_CHANGE);
+        mController.unregisterWalletChangeObservers(DEFAULT_PAYMENT_APP_CHANGE,
+                DEFAULT_WALLET_APP_CHANGE);
     }
 
     private class WalletCardRetriever implements
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/wallet/controller/QuickAccessWalletController.java b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
index e031be2..e0228d9 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
@@ -17,10 +17,13 @@
 package com.android.systemui.wallet.controller;
 
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
+import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE;
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE;
 
 import android.annotation.WorkerThread;
 import android.app.PendingIntent;
+import android.app.role.OnRoleHoldersChangedListener;
+import android.app.role.RoleManager;
 import android.content.Context;
 import android.content.Intent;
 import android.database.ContentObserver;
@@ -31,12 +34,12 @@
 import android.service.quickaccesswallet.QuickAccessWalletClientImpl;
 import android.util.Log;
 
-import com.android.systemui.res.R;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.SystemClock;
 import com.android.systemui.wallet.ui.WalletActivity;
@@ -58,6 +61,7 @@
      */
     public enum WalletChangeEvent {
         DEFAULT_PAYMENT_APP_CHANGE,
+        DEFAULT_WALLET_APP_CHANGE,
         WALLET_PREFERENCE_CHANGE,
     }
 
@@ -71,9 +75,12 @@
 
     private QuickAccessWalletClient mQuickAccessWalletClient;
     private ContentObserver mWalletPreferenceObserver;
+    private RoleManager mRoleManager;
+    private OnRoleHoldersChangedListener mDefaultWalletAppObserver;
     private ContentObserver mDefaultPaymentAppObserver;
     private int mWalletPreferenceChangeEvents = 0;
     private int mDefaultPaymentAppChangeEvents = 0;
+    private int mDefaultWalletAppChangeEvents = 0;
     private boolean mWalletEnabled = false;
     private long mQawClientCreatedTimeMillis;
 
@@ -89,6 +96,7 @@
         mExecutor = executor;
         mBgExecutor = bgExecutor;
         mSecureSettings = secureSettings;
+        mRoleManager = mContext.getSystemService(RoleManager.class);
         mQuickAccessWalletClient = quickAccessWalletClient;
         mClock = clock;
         mQawClientCreatedTimeMillis = mClock.elapsedRealtime();
@@ -122,6 +130,8 @@
                 setupWalletPreferenceObserver();
             } else if (event == DEFAULT_PAYMENT_APP_CHANGE) {
                 setupDefaultPaymentAppObserver(cardsRetriever);
+            } else if (event == DEFAULT_WALLET_APP_CHANGE) {
+                setupDefaultWalletAppObserver(cardsRetriever);
             }
         }
     }
@@ -141,6 +151,12 @@
                 if (mDefaultPaymentAppChangeEvents == 0) {
                     mSecureSettings.unregisterContentObserver(mDefaultPaymentAppObserver);
                 }
+            } else if (event == DEFAULT_WALLET_APP_CHANGE && mDefaultWalletAppObserver != null) {
+                mDefaultWalletAppChangeEvents--;
+                if (mDefaultWalletAppChangeEvents == 0) {
+                    mRoleManager.removeOnRoleHoldersChangedListenerAsUser(mDefaultWalletAppObserver,
+                            UserHandle.ALL);
+                }
             }
         }
     }
@@ -300,6 +316,25 @@
         mDefaultPaymentAppChangeEvents++;
     }
 
+    private void setupDefaultWalletAppObserver(
+            QuickAccessWalletClient.OnWalletCardsRetrievedCallback cardsRetriever) {
+        if (mDefaultWalletAppObserver == null) {
+            mDefaultWalletAppObserver = (roleName, user) -> {
+                if (!roleName.equals(RoleManager.ROLE_WALLET)) {
+                    return;
+                }
+                mExecutor.execute(() -> {
+                    reCreateWalletClient();
+                    updateWalletPreference();
+                    queryWalletCards(cardsRetriever);
+                });
+            };
+            mRoleManager.addOnRoleHoldersChangedListenerAsUser(mExecutor,
+                    mDefaultWalletAppObserver, UserHandle.ALL);
+        }
+        mDefaultWalletAppChangeEvents++;
+    }
+
     private void setupWalletPreferenceObserver() {
         if (mWalletPreferenceObserver == null) {
             mWalletPreferenceObserver = new ContentObserver(null /* handler */) {
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
index 75df1bd..eb4ff17 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
@@ -84,7 +84,9 @@
                         walletController.setupWalletChangeObservers(
                             callback,
                             QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
-                            QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+                            QuickAccessWalletController.WalletChangeEvent
+                                .DEFAULT_PAYMENT_APP_CHANGE,
+                            QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE
                         )
                         walletController.updateWalletPreference()
                         walletController.queryWalletCards(callback, MAX_CARDS)
@@ -94,7 +96,9 @@
                                 QuickAccessWalletController.WalletChangeEvent
                                     .WALLET_PREFERENCE_CHANGE,
                                 QuickAccessWalletController.WalletChangeEvent
-                                    .DEFAULT_PAYMENT_APP_CHANGE
+                                    .DEFAULT_PAYMENT_APP_CHANGE,
+                                QuickAccessWalletController.WalletChangeEvent
+                                    .DEFAULT_WALLET_APP_CHANGE
                             )
                         }
                     }
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/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/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/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/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/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/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/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/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/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
index a763251..542165d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
@@ -46,7 +46,7 @@
     private static final String TAG = "InputMethodInfoUtils";
 
     /**
-     * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs
+     * Used in {@link #getFallbackLocaleForDefaultIme(List, Context)} to find the fallback IMEs
      * that are mainly used until the system becomes ready. Note that {@link Locale} in this array
      * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH}
      * doesn't automatically match {@code Locale("en", "IN")}.
@@ -64,7 +64,7 @@
         @NonNull
         private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>();
 
-        InputMethodListBuilder fillImes(ArrayList<InputMethodInfo> imis, Context context,
+        InputMethodListBuilder fillImes(List<InputMethodInfo> imis, Context context,
                 boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry,
                 String requiredSubtypeMode) {
             for (int i = 0; i < imis.size(); ++i) {
@@ -77,7 +77,7 @@
             return this;
         }
 
-        InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) {
+        InputMethodListBuilder fillAuxiliaryImes(List<InputMethodInfo> imis, Context context) {
             // If one or more auxiliary input methods are available, OK to stop populating the list.
             for (final InputMethodInfo imi : mInputMethodSet) {
                 if (imi.isAuxiliaryIme()) {
@@ -118,7 +118,7 @@
     }
 
     private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale(
-            ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale,
+            List<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale,
             @Nullable Locale fallbackLocale) {
         // Once the system becomes ready, we pick up at least one keyboard in the following order.
         // Secondary users fall into this category in general.
@@ -167,7 +167,7 @@
     }
 
     static ArrayList<InputMethodInfo> getDefaultEnabledImes(
-            Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum) {
+            Context context, List<InputMethodInfo> imis, boolean onlyMinimum) {
         final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
         // We will primarily rely on the system locale, but also keep relying on the fallback locale
         // as a last resort.
@@ -186,7 +186,7 @@
     }
 
     static ArrayList<InputMethodInfo> getDefaultEnabledImes(
-            Context context, ArrayList<InputMethodInfo> imis) {
+            Context context, List<InputMethodInfo> imis) {
         return getDefaultEnabledImes(context, imis, false /* onlyMinimum */);
     }
 
@@ -283,7 +283,7 @@
     }
 
     @Nullable
-    private static Locale getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis,
+    private static Locale getFallbackLocaleForDefaultIme(List<InputMethodInfo> imis,
             Context context) {
         // At first, find the fallback locale from the IMEs that are declared as "default" in the
         // current locale.  Note that IME developers can declare an IME as "default" only for
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/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/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index c5b5a76..609b3aa 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4608,6 +4608,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 +4628,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/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/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/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/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
index 7cbfc52..570132f 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,10 +124,12 @@
 
     private List<InputMethodInfo> filterInputMethodServices(List<ResolveInfo> resolveInfoList,
             List<String> enabledComponents) {
+        final ArrayMap<String, List<InputMethodSubtype>> emptyAdditionalSubtypeMap =
+                new ArrayMap<>();
         final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
         final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
-        InputMethodManagerService.filterInputMethodServices(new ArrayMap<>(), methodMap, methodList,
-                enabledComponents, mContext, resolveInfoList);
+        InputMethodManagerService.filterInputMethodServices(emptyAdditionalSubtypeMap, methodMap,
+                methodList, enabledComponents, mContext, resolveInfoList);
         return methodList;
     }
 
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/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/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/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/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