Merge "Send update from NMS to SysUI on LifetimeExtension" into main
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 7de6799..52c0ac1 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -124,6 +124,15 @@
     @Overridable // Aid in testing
     public static final long ENFORCE_MINIMUM_TIME_WINDOWS = 311402873L;
 
+    /**
+     * Require that minimum latencies and override deadlines are nonnegative.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long REJECT_NEGATIVE_DELAYS_AND_DEADLINES = 323349338L;
+
     /** @hide */
     @IntDef(prefix = { "NETWORK_TYPE_" }, value = {
             NETWORK_TYPE_NONE,
@@ -692,14 +701,14 @@
      * @see JobInfo.Builder#setMinimumLatency(long)
      */
     public long getMinLatencyMillis() {
-        return minLatencyMillis;
+        return Math.max(0, minLatencyMillis);
     }
 
     /**
      * @see JobInfo.Builder#setOverrideDeadline(long)
      */
     public long getMaxExecutionDelayMillis() {
-        return maxExecutionDelayMillis;
+        return Math.max(0, maxExecutionDelayMillis);
     }
 
     /**
@@ -818,7 +827,7 @@
      * @hide
      */
     public boolean hasEarlyConstraint() {
-        return hasEarlyConstraint;
+        return hasEarlyConstraint && minLatencyMillis > 0;
     }
 
     /**
@@ -827,7 +836,7 @@
      * @hide
      */
     public boolean hasLateConstraint() {
-        return hasLateConstraint;
+        return hasLateConstraint && maxExecutionDelayMillis >= 0;
     }
 
     @Override
@@ -1869,6 +1878,13 @@
          * Because it doesn't make sense setting this property on a periodic job, doing so will
          * throw an {@link java.lang.IllegalArgumentException} when
          * {@link android.app.job.JobInfo.Builder#build()} is called.
+         *
+         * Negative latencies also don't make sense for a job and are indicative of an error,
+         * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
+         * setting a negative deadline will result in
+         * {@link android.app.job.JobInfo.Builder#build()} throwing an
+         * {@link java.lang.IllegalArgumentException}.
+         *
          * @param minLatencyMillis Milliseconds before which this job will not be considered for
          *                         execution.
          * @see JobInfo#getMinLatencyMillis()
@@ -1892,6 +1908,13 @@
          * throw an {@link java.lang.IllegalArgumentException} when
          * {@link android.app.job.JobInfo.Builder#build()} is called.
          *
+         * <p>
+         * Negative deadlines also don't make sense for a job and are indicative of an error,
+         * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
+         * setting a negative deadline will result in
+         * {@link android.app.job.JobInfo.Builder#build()} throwing an
+         * {@link java.lang.IllegalArgumentException}.
+         *
          * <p class="note">
          * Since a job will run once the deadline has passed regardless of the status of other
          * constraints, setting a deadline of 0 (or a {@link #setMinimumLatency(long) delay} equal
@@ -2189,13 +2212,15 @@
         public JobInfo build() {
             return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS),
                     Compatibility.isChangeEnabled(REJECT_NEGATIVE_NETWORK_ESTIMATES),
-                    Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS));
+                    Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS),
+                    Compatibility.isChangeEnabled(REJECT_NEGATIVE_DELAYS_AND_DEADLINES));
         }
 
         /** @hide */
         public JobInfo build(boolean disallowPrefetchDeadlines,
                 boolean rejectNegativeNetworkEstimates,
-                boolean enforceMinimumTimeWindows) {
+                boolean enforceMinimumTimeWindows,
+                boolean rejectNegativeDelaysAndDeadlines) {
             // This check doesn't need to be inside enforceValidity. It's an unnecessary legacy
             // check that would ideally be phased out instead.
             if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
@@ -2205,7 +2230,7 @@
             }
             JobInfo jobInfo = new JobInfo(this);
             jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates,
-                    enforceMinimumTimeWindows);
+                    enforceMinimumTimeWindows, rejectNegativeDelaysAndDeadlines);
             return jobInfo;
         }
 
@@ -2225,7 +2250,8 @@
      */
     public final void enforceValidity(boolean disallowPrefetchDeadlines,
             boolean rejectNegativeNetworkEstimates,
-            boolean enforceMinimumTimeWindows) {
+            boolean enforceMinimumTimeWindows,
+            boolean rejectNegativeDelaysAndDeadlines) {
         // Check that network estimates require network type and are reasonable values.
         if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0)
                 && networkRequest == null) {
@@ -2259,6 +2285,17 @@
             throw new IllegalArgumentException("Minimum chunk size must be positive");
         }
 
+        if (rejectNegativeDelaysAndDeadlines) {
+            if (minLatencyMillis < 0) {
+                throw new IllegalArgumentException(
+                        "Minimum latency is negative: " + minLatencyMillis);
+            }
+            if (maxExecutionDelayMillis < 0) {
+                throw new IllegalArgumentException(
+                        "Override deadline is negative: " + maxExecutionDelayMillis);
+            }
+        }
+
         final boolean hasDeadline = maxExecutionDelayMillis != 0L;
         // Check that a deadline was not set on a periodic job.
         if (isPeriodic) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index a83c099..f819f15 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -4850,7 +4850,7 @@
                     Slog.w(TAG, "Uid " + uid + " set bias on its job");
                     return new JobInfo.Builder(job)
                             .setBias(JobInfo.BIAS_DEFAULT)
-                            .build(false, false, false);
+                            .build(false, false, false, false);
                 }
             }
 
@@ -4874,7 +4874,9 @@
                             JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid),
                     rejectNegativeNetworkEstimates,
                     CompatChanges.isChangeEnabled(
-                            JobInfo.ENFORCE_MINIMUM_TIME_WINDOWS, callingUid));
+                            JobInfo.ENFORCE_MINIMUM_TIME_WINDOWS, callingUid),
+                    CompatChanges.isChangeEnabled(
+                            JobInfo.REJECT_NEGATIVE_DELAYS_AND_DEADLINES, callingUid));
             if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index 53b14d6..d8934d8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -1495,7 +1495,7 @@
                 // return value), the deadline is dropped. Periodic jobs require all constraints
                 // to be met, so there's no issue with their deadlines.
                 // The same logic applies for other target SDK-based validation checks.
-                builtJob = jobBuilder.build(false, false, false);
+                builtJob = jobBuilder.build(false, false, false, false);
             } catch (Exception e) {
                 Slog.w(TAG, "Unable to build job from XML, ignoring: " + jobBuilder.summarize(), e);
                 return null;
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 6883d18..aec464d 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
@@ -241,6 +241,8 @@
         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;
+        private long mCachedScoreExpirationTimeElapsed;
+        private int mCachedScore;
 
         public void addScore(int add, long nowElapsed) {
             JobScoreBucket bucket = mScoreBuckets[mScoreBucketIndex];
@@ -248,10 +250,17 @@
                 bucket = new JobScoreBucket();
                 bucket.startTimeElapsed = nowElapsed;
                 mScoreBuckets[mScoreBucketIndex] = bucket;
+                // Brand new bucket, there's nothing to remove from the score,
+                // so just update the expiration time if needed.
+                mCachedScoreExpirationTimeElapsed = Math.min(mCachedScoreExpirationTimeElapsed,
+                        nowElapsed + MAX_TIME_WINDOW_MS);
             } else if (bucket.startTimeElapsed < nowElapsed - MAX_TIME_WINDOW_MS) {
                 // The bucket is too old.
                 bucket.reset();
                 bucket.startTimeElapsed = nowElapsed;
+                // Force a recalculation of the cached score instead of just updating the cached
+                // value and time in case there are multiple stale buckets.
+                mCachedScoreExpirationTimeElapsed = 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.
@@ -261,16 +270,26 @@
             }
 
             bucket.score += add;
+            mCachedScore += add;
         }
 
         public int getScore(long nowElapsed) {
+            if (nowElapsed < mCachedScoreExpirationTimeElapsed) {
+                return mCachedScore;
+            }
             int score = 0;
             final long earliestElapsed = nowElapsed - MAX_TIME_WINDOW_MS;
+            long earliestValidBucketTimeElapsed = Long.MAX_VALUE;
             for (JobScoreBucket bucket : mScoreBuckets) {
                 if (bucket != null && bucket.startTimeElapsed >= earliestElapsed) {
                     score += bucket.score;
+                    if (earliestValidBucketTimeElapsed > bucket.startTimeElapsed) {
+                        earliestValidBucketTimeElapsed = bucket.startTimeElapsed;
+                    }
                 }
             }
+            mCachedScore = score;
+            mCachedScoreExpirationTimeElapsed = earliestValidBucketTimeElapsed + MAX_TIME_WINDOW_MS;
             return score;
         }
 
@@ -378,10 +397,16 @@
 
     @Override
     public void prepareForExecutionLocked(JobStatus jobStatus) {
+        if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
+            // Don't include jobs for the TOP app in the score calculation.
+            return;
+        }
         // 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);
+        final int priority = jobStatus.getJob().getPriority();
+        final int score = mFallbackFlexibilityDeadlineScores.get(priority,
+                FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES
+                        .get(priority, priority / 100));
         JobScoreTracker jobScoreTracker =
                 mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName());
         if (jobScoreTracker == null) {
@@ -394,6 +419,10 @@
 
     @Override
     public void unprepareFromExecutionLocked(JobStatus jobStatus) {
+        if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
+            // Jobs for the TOP app are excluded from the score calculation.
+            return;
+        }
         // The job didn't actually start. Undo the score increase.
         JobScoreTracker jobScoreTracker =
                 mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName());
@@ -401,8 +430,10 @@
             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);
+        final int priority = jobStatus.getJob().getPriority();
+        final int score = mFallbackFlexibilityDeadlineScores.get(priority,
+                FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES
+                        .get(priority, priority / 100));
         jobScoreTracker.addScore(-score, sElapsedRealtimeClock.millis());
     }
 
@@ -649,21 +680,24 @@
                     (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2),
                     mMaxRescheduledDeadline);
         }
+
+        // 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 fallbackDurationMs = Math.min(3 * mFallbackFlexibilityDeadlineMs,
+                mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs)
+                        + mFallbackFlexibilityAdditionalScoreTimeFactors
+                                .get(jobPriority, MINUTE_IN_MILLIS) * jobScore);
+        final long fallbackDeadlineMs = earliest + fallbackDurationMs;
+
         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 fallbackDeadlineMs;
         }
-        return js.getLatestRunTimeElapsed();
+        return Math.max(fallbackDeadlineMs, js.getLatestRunTimeElapsed());
     }
 
     @VisibleForTesting
@@ -976,7 +1010,8 @@
                     // Something has gone horribly wrong. This has only occurred on incorrectly
                     // configured tests, but add a check here for safety.
                     Slog.wtf(TAG, "Got invalid latest when scheduling alarm."
-                            + " Prefetch=" + js.getJob().isPrefetch());
+                            + " prefetch=" + js.getJob().isPrefetch()
+                            + " periodic=" + js.getJob().isPeriodic());
                     // Since things have gone wrong, the safest and most reliable thing to do is
                     // stop applying flex policy to the job.
                     mFlexibilityTracker.setNumDroppedFlexibleConstraints(js,
@@ -991,7 +1026,7 @@
 
                 if (DEBUG) {
                     Slog.d(TAG, "scheduleDropNumConstraintsAlarm: "
-                            + js.getSourcePackageName() + " " + js.getSourceUserId()
+                            + js.toShortString()
                             + " numApplied: " + js.getNumAppliedFlexibleConstraints()
                             + " numRequired: " + js.getNumRequiredFlexibleConstraints()
                             + " numSatisfied: " + Integer.bitCount(
@@ -1199,11 +1234,11 @@
             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);
+                    .put(PRIORITY_HIGH, 3 * MINUTE_IN_MILLIS);
             DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
-                    .put(PRIORITY_DEFAULT, 3 * MINUTE_IN_MILLIS);
+                    .put(PRIORITY_DEFAULT, 2 * MINUTE_IN_MILLIS);
             DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
-                    .put(PRIORITY_LOW, 2 * MINUTE_IN_MILLIS);
+                    .put(PRIORITY_LOW, 1 * MINUTE_IN_MILLIS);
             DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
                     .put(PRIORITY_MIN, 1 * MINUTE_IN_MILLIS);
             DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
@@ -1220,7 +1255,7 @@
 
         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;
+        private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = DAY_IN_MILLIS;
         @VisibleForTesting
         static final long DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = 3 * DAY_IN_MILLIS;
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index a3a686f..edd86e3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -16,8 +16,6 @@
 
 package com.android.server.job.controllers;
 
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-
 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.NEVER_INDEX;
@@ -430,9 +428,6 @@
      */
     public static final int INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ = 1 << 2;
 
-    /** Minimum difference between start and end time to have flexible constraint */
-    @VisibleForTesting
-    static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS;
     /**
      * Versatile, persistable flags for a job that's updated within the system server,
      * as opposed to {@link JobInfo#flags} that's set by callers.
@@ -657,7 +652,7 @@
                     .build());
             // Don't perform validation checks at this point since we've already passed the
             // initial validation check.
-            job = builder.build(false, false, false);
+            job = builder.build(false, false, false, false);
         }
 
         this.job = job;
@@ -708,14 +703,10 @@
         final boolean lacksSomeFlexibleConstraints =
                 ((~requiredConstraints) & SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS) != 0
                         || mCanApplyTransportAffinities;
-        final boolean satisfiesMinWindowException =
-                (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis)
-                >= MIN_WINDOW_FOR_FLEXIBILITY_MS;
 
         // The first time a job is rescheduled it will not be subject to flexible constraints.
         // Otherwise, every consecutive reschedule increases a jobs' flexibility deadline.
         if (!isRequestedExpeditedJob() && !job.isUserInitiated()
-                && satisfiesMinWindowException
                 && (numFailures + numSystemStops) != 1
                 && lacksSomeFlexibleConstraints) {
             requiredConstraints |= CONSTRAINT_FLEXIBLE;
diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
index 4dc4b68..5239fbc 100644
--- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
+++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
@@ -455,7 +455,7 @@
         assertThat(regEvent.getBus()).isEqualTo(0x001d);
         assertThat(regEvent.getVendorId()).isEqualTo(0x6cb);
         assertThat(regEvent.getProductId()).isEqualTo(0x0000);
-        // TODO(b/302297266): check version ID once it's supported
+        assertThat(regEvent.getVersionId()).isEqualTo(0x0000);
 
         assertThat(regEvent.getConfiguration().get(UinputControlCode.UI_SET_PROPBIT.getValue()))
                 .asList().containsExactly(0, 2);
diff --git a/core/api/current.txt b/core/api/current.txt
index 41f96ee..36d7145 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1804,6 +1804,7 @@
     field public static final int useEmbeddedDex = 16844190; // 0x101059e
     field public static final int useIntrinsicSizeAsMinimum = 16843536; // 0x1010310
     field public static final int useLevel = 16843167; // 0x101019f
+    field @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public static final int useLocalePreferredLineHeightForMinimum;
     field public static final int userVisible = 16843409; // 0x1010291
     field public static final int usesCleartextTraffic = 16844012; // 0x10104ec
     field public static final int usesPermissionFlags = 16844356; // 0x1010644
@@ -3322,11 +3323,13 @@
     method @FlaggedApi("android.view.accessibility.a11y_overlay_callbacks") public void attachAccessibilityOverlayToWindow(int, @NonNull android.view.SurfaceControl, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
     method public boolean clearCache();
     method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void clearTestBrailleDisplayController();
     method public final void disableSelf();
     method public final boolean dispatchGesture(@NonNull android.accessibilityservice.GestureDescription, @Nullable android.accessibilityservice.AccessibilityService.GestureResultCallback, @Nullable android.os.Handler);
     method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
     method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController();
     method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(int);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @NonNull public android.accessibilityservice.BrailleDisplayController getBrailleDisplayController();
     method @NonNull @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController();
     method @Nullable public final android.accessibilityservice.InputMethod getInputMethod();
     method @NonNull public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController();
@@ -3356,6 +3359,7 @@
     method public boolean setCacheEnabled(boolean);
     method public void setGestureDetectionPassthroughRegion(int, @NonNull android.graphics.Region);
     method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void setTestBrailleDisplayController(@NonNull android.accessibilityservice.BrailleDisplayController);
     method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region);
     method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
     method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
@@ -3560,6 +3564,25 @@
     field public String[] packageNames;
   }
 
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void disconnect();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public boolean isConnected();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void write(@NonNull byte[]) throws java.io.IOException;
+  }
+
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public static interface BrailleDisplayController.BrailleDisplayCallback {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnected(@NonNull byte[]);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnectionFailed(int);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onDisconnected();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onInput(@NonNull byte[]);
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 2; // 0x2
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_CANNOT_ACCESS = 1; // 0x1
+  }
+
   public final class FingerprintGestureController {
     method public boolean isGestureDetectionAvailable();
     method public void registerFingerprintGestureCallback(@NonNull android.accessibilityservice.FingerprintGestureController.FingerprintGestureCallback, @Nullable android.os.Handler);
@@ -25846,6 +25869,9 @@
     method public int describeContents();
     method public int getErrorCode();
     method public int getFinalState();
+    method @NonNull public java.util.List<android.media.metrics.MediaItemInfo> getInputMediaItemInfos();
+    method public long getOperationTypes();
+    method @Nullable public android.media.metrics.MediaItemInfo getOutputMediaItemInfo();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.EditingEndedEvent> CREATOR;
     field public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; // 0x12
@@ -25870,14 +25896,25 @@
     field public static final int FINAL_STATE_CANCELED = 2; // 0x2
     field public static final int FINAL_STATE_ERROR = 3; // 0x3
     field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1
+    field public static final long OPERATION_TYPE_AUDIO_EDIT = 8L; // 0x8L
+    field public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 2L; // 0x2L
+    field public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 32L; // 0x20L
+    field public static final long OPERATION_TYPE_PAUSED = 64L; // 0x40L
+    field public static final long OPERATION_TYPE_RESUMED = 128L; // 0x80L
+    field public static final long OPERATION_TYPE_VIDEO_EDIT = 4L; // 0x4L
+    field public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1L; // 0x1L
+    field public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 16L; // 0x10L
     field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff
   }
 
   @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder {
     ctor public EditingEndedEvent.Builder(int);
+    method @NonNull public android.media.metrics.EditingEndedEvent.Builder addInputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo);
+    method @NonNull public android.media.metrics.EditingEndedEvent.Builder addOperationType(long);
     method @NonNull public android.media.metrics.EditingEndedEvent build();
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int);
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle);
+    method @NonNull public android.media.metrics.EditingEndedEvent.Builder setOutputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo);
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long);
   }
 
@@ -25897,6 +25934,65 @@
     field @NonNull public static final android.media.metrics.LogSessionId LOG_SESSION_ID_NONE;
   }
 
+  @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public final class MediaItemInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioChannelCount();
+    method public long getAudioSampleCount();
+    method public int getAudioSampleRateHz();
+    method public long getClipDurationMillis();
+    method @NonNull public java.util.List<java.lang.String> getCodecNames();
+    method @Nullable public String getContainerMimeType();
+    method public long getDataTypes();
+    method public long getDurationMillis();
+    method @NonNull public java.util.List<java.lang.String> getSampleMimeTypes();
+    method public int getSourceType();
+    method public int getVideoDataSpace();
+    method public float getVideoFrameRate();
+    method public long getVideoSampleCount();
+    method @NonNull public android.util.Size getVideoSize();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.MediaItemInfo> CREATOR;
+    field public static final long DATA_TYPE_AUDIO = 4L; // 0x4L
+    field public static final long DATA_TYPE_CUE_POINTS = 128L; // 0x80L
+    field public static final long DATA_TYPE_DEPTH = 16L; // 0x10L
+    field public static final long DATA_TYPE_GAIN_MAP = 32L; // 0x20L
+    field public static final long DATA_TYPE_GAPLESS = 256L; // 0x100L
+    field public static final long DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO = 1024L; // 0x400L
+    field public static final long DATA_TYPE_HIGH_FRAME_RATE = 64L; // 0x40L
+    field public static final long DATA_TYPE_IMAGE = 1L; // 0x1L
+    field public static final long DATA_TYPE_METADATA = 8L; // 0x8L
+    field public static final long DATA_TYPE_SPATIAL_AUDIO = 512L; // 0x200L
+    field public static final long DATA_TYPE_VIDEO = 2L; // 0x2L
+    field public static final int SOURCE_TYPE_CAMERA = 2; // 0x2
+    field public static final int SOURCE_TYPE_EDITING_SESSION = 3; // 0x3
+    field public static final int SOURCE_TYPE_GALLERY = 1; // 0x1
+    field public static final int SOURCE_TYPE_GENERATED = 7; // 0x7
+    field public static final int SOURCE_TYPE_LOCAL_FILE = 4; // 0x4
+    field public static final int SOURCE_TYPE_REMOTE_FILE = 5; // 0x5
+    field public static final int SOURCE_TYPE_REMOTE_LIVE_STREAM = 6; // 0x6
+    field public static final int SOURCE_TYPE_UNSPECIFIED = 0; // 0x0
+    field public static final int VALUE_UNSPECIFIED = -1; // 0xffffffff
+  }
+
+  @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class MediaItemInfo.Builder {
+    ctor public MediaItemInfo.Builder();
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder addCodecName(@NonNull String);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder addDataType(long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder addSampleMimeType(@NonNull String);
+    method @NonNull public android.media.metrics.MediaItemInfo build();
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioChannelCount(@IntRange(from=0) int);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioSampleCount(@IntRange(from=0) long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioSampleRateHz(@IntRange(from=0) int);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setClipDurationMillis(long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setContainerMimeType(@NonNull String);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setDurationMillis(long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setSourceType(int);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoDataSpace(int);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoFrameRate(@FloatRange(from=0) float);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoSampleCount(@IntRange(from=0) long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoSize(@NonNull android.util.Size);
+  }
+
   public final class MediaMetricsManager {
     method @NonNull public android.media.metrics.BundleSession createBundleSession();
     method @NonNull public android.media.metrics.EditingSession createEditingSession();
@@ -35463,7 +35559,7 @@
     field public static final String LONGITUDE = "longitude";
   }
 
-  @FlaggedApi("android.provider.user_keys") public class ContactKeysManager {
+  @FlaggedApi("android.provider.user_keys") public final class ContactKeysManager {
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.ContactKey> getAllContactKeys(@NonNull String);
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.SelfKey> getAllSelfKeys();
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public android.provider.ContactKeysManager.ContactKey getContactKey(@NonNull String, @NonNull String, @NonNull String);
@@ -35478,9 +35574,9 @@
     method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public void updateOrInsertContactKey(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
     method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateOrInsertSelfKey(@NonNull String, @NonNull String, @NonNull byte[]);
     method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, int);
-    field public static final int UNVERIFIED = 0; // 0x0
-    field public static final int VERIFICATION_FAILED = 1; // 0x1
-    field public static final int VERIFIED = 2; // 0x2
+    field public static final int VERIFICATION_STATE_UNVERIFIED = 0; // 0x0
+    field public static final int VERIFICATION_STATE_VERIFICATION_FAILED = 1; // 0x1
+    field public static final int VERIFICATION_STATE_VERIFIED = 2; // 0x2
   }
 
   public static final class ContactKeysManager.ContactKey implements android.os.Parcelable {
@@ -52220,6 +52316,7 @@
     method public final boolean getClipToOutline();
     method @Nullable public final android.view.contentcapture.ContentCaptureSession getContentCaptureSession();
     method public CharSequence getContentDescription();
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final int getContentSensitivity();
     method @UiContext public final android.content.Context getContext();
     method protected android.view.ContextMenu.ContextMenuInfo getContextMenuInfo();
     method public final boolean getDefaultFocusHighlightEnabled();
@@ -52399,6 +52496,7 @@
     method public boolean isAttachedToWindow();
     method public boolean isAutoHandwritingEnabled();
     method public boolean isClickable();
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final boolean isContentSensitive();
     method public boolean isContextClickable();
     method public boolean isCredential();
     method public boolean isDirty();
@@ -52603,6 +52701,7 @@
     method public void setClipToOutline(boolean);
     method public void setContentCaptureSession(@Nullable android.view.contentcapture.ContentCaptureSession);
     method public void setContentDescription(CharSequence);
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final void setContentSensitivity(int);
     method public void setContextClickable(boolean);
     method public void setDefaultFocusHighlightEnabled(boolean);
     method @Deprecated public void setDrawingCacheBackgroundColor(@ColorInt int);
@@ -52787,6 +52886,9 @@
     field public static final int AUTOFILL_TYPE_NONE = 0; // 0x0
     field public static final int AUTOFILL_TYPE_TEXT = 1; // 0x1
     field public static final int AUTOFILL_TYPE_TOGGLE = 2; // 0x2
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_AUTO = 0; // 0x0
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 2; // 0x2
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_SENSITIVE = 1; // 0x1
     field public static final int DRAG_FLAG_ACCESSIBILITY_ACTION = 1024; // 0x400
     field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100
     field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40
@@ -60507,6 +60609,7 @@
     method public boolean isFallbackLineSpacing();
     method public final boolean isHorizontallyScrollable();
     method public boolean isInputMethodTarget();
+    method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public boolean isLocalePreferredLineHeightForMinimumUsed();
     method public boolean isSingleLine();
     method public boolean isSuggestionsEnabled();
     method public boolean isTextSelectable();
@@ -60589,6 +60692,7 @@
     method public final void setLinkTextColor(@ColorInt int);
     method public final void setLinkTextColor(android.content.res.ColorStateList);
     method public final void setLinksClickable(boolean);
+    method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void setLocalePreferredLineHeightForMinimumUsed(boolean);
     method public void setMarqueeRepeatLimit(int);
     method public void setMaxEms(int);
     method public void setMaxHeight(int);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index aca003d..ecbdeaa 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11467,7 +11467,7 @@
     field public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
-  @FlaggedApi("android.provider.user_keys") public class ContactKeysManager {
+  @FlaggedApi("android.provider.user_keys") public final class ContactKeysManager {
     method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyLocalVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int);
     method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int);
     method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, int);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 288374d..19b265d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -104,6 +104,14 @@
     method @FlaggedApi("android.view.accessibility.motion_event_observing") public void setObservedMotionEventSources(int);
   }
 
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public static void setTestBrailleDisplayData(@NonNull android.accessibilityservice.AccessibilityService, @NonNull java.util.List<android.os.Bundle>);
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+  }
+
 }
 
 package android.animation {
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index e7e3a85..f7d7522 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -80,6 +80,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
@@ -850,6 +851,8 @@
     private boolean mInputMethodInitialized = false;
     private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers =
             new SparseArray<>(0);
+    private BrailleDisplayController mBrailleDisplayController;
+    private BrailleDisplayController mTestBrailleDisplayController;
 
     private int mGestureStatusCallbackSequence;
 
@@ -3634,4 +3637,56 @@
                 .attachAccessibilityOverlayToWindow(
                         mConnectionId, accessibilityWindowId, sc, executor, callback);
     }
+
+    /**
+     * Returns the {@link BrailleDisplayController} which may be used to communicate with
+     * refreshable Braille displays that provide USB or Bluetooth Braille display HID support.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @NonNull
+    public BrailleDisplayController getBrailleDisplayController() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            if (mTestBrailleDisplayController != null) {
+                return mTestBrailleDisplayController;
+            }
+
+            if (mBrailleDisplayController == null) {
+                mBrailleDisplayController = new BrailleDisplayControllerImpl(this, mLock);
+            }
+            return mBrailleDisplayController;
+        }
+    }
+
+    /**
+     * Set the {@link BrailleDisplayController} implementation that will be returned by
+     * {@link #getBrailleDisplayController}, to allow this accessibility service to test its
+     * interaction with BrailleDisplayController without requiring a real Braille display.
+     *
+     * <p>For full test fidelity, ensure that this test-only implementation follows the same
+     * behavior specified in the documentation for {@link BrailleDisplayController}, including
+     * thrown exceptions.
+     *
+     * @param controller A test-only implementation of {@link BrailleDisplayController}.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void setTestBrailleDisplayController(@NonNull BrailleDisplayController controller) {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        Objects.requireNonNull(controller);
+        synchronized (mLock) {
+            mTestBrailleDisplayController = controller;
+        }
+    }
+
+    /**
+     * Clears the {@link BrailleDisplayController} previously set by
+     * {@link #setTestBrailleDisplayController}.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void clearTestBrailleDisplayController() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            mTestBrailleDisplayController = null;
+        }
+    }
 }
diff --git a/core/java/android/accessibilityservice/BrailleDisplayController.java b/core/java/android/accessibilityservice/BrailleDisplayController.java
new file mode 100644
index 0000000..5282aa3
--- /dev/null
+++ b/core/java/android/accessibilityservice/BrailleDisplayController.java
@@ -0,0 +1,308 @@
+/*
+ * 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.accessibilityservice;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.Flags;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Used to communicate with a Braille display that supports the Braille display HID standard
+ * (usage page 0x41).
+ *
+ * <p>Only one Braille display may be connected at a time.
+ */
+// This interface doesn't actually own resources. Its I/O connections are owned, monitored,
+// and automatically closed by the system after the accessibility service is disconnected.
+@SuppressLint("NotCloseable")
+@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+public interface BrailleDisplayController {
+
+    /**
+     * Throw {@link IllegalStateException} if this feature's aconfig flag is disabled.
+     *
+     * @hide
+     */
+    static void checkApiFlagIsEnabled() {
+        if (!Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+    }
+
+    /**
+     * Interface provided to {@link BrailleDisplayController} connection methods to
+     * receive callbacks from the system.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    interface BrailleDisplayCallback {
+        /**
+         * The system cannot access connected HID devices.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        int FLAG_ERROR_CANNOT_ACCESS = 1 << 0;
+        /**
+         * A unique Braille display matching the requested properties could not be identified.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 1 << 1;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = "FLAG_ERROR_", value = {
+                FLAG_ERROR_CANNOT_ACCESS,
+                FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND,
+        })
+        @interface ErrorCode {
+        }
+
+        /**
+         * Callback to observe a successful Braille display connection.
+         *
+         * <p>The provided HID report descriptor should be used to understand the input bytes
+         * received from the Braille display via {@link #onInput} and to prepare
+         * the output sent to the Braille display via {@link #write}.
+         *
+         * @param hidDescriptor The HID report descriptor for this Braille display.
+         * @see #connect(BluetoothDevice, BrailleDisplayCallback)
+         * @see #connect(UsbDevice, BrailleDisplayCallback)
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onConnected(@NonNull byte[] hidDescriptor);
+
+        /**
+         * Callback to observe a failed Braille display connection.
+         *
+         * @param errorFlags A bitmask of error codes for the connection failure.
+         * @see #connect(BluetoothDevice, BrailleDisplayCallback)
+         * @see #connect(UsbDevice, BrailleDisplayCallback)
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onConnectionFailed(@ErrorCode int errorFlags);
+
+        /**
+         * Callback to observe input bytes from the currently connected Braille display.
+         *
+         * @param input The input bytes from the Braille display, formatted according to the HID
+         *              report descriptor and the HIDRAW kernel driver.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onInput(@NonNull byte[] input);
+
+        /**
+         * Callback to observe when the currently connected Braille display is disconnected by the
+         * system.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onDisconnected();
+    }
+
+    /**
+     * Connects to the requested bluetooth Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>Note that the callbacks will be executed on the main thread using
+     * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
+     * {@link #connect(BluetoothDevice, Executor, BrailleDisplayCallback)}.
+     *
+     * @param bluetoothDevice The Braille display device.
+     * @param callback        Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested bluetooth Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * @param bluetoothDevice  The Braille display device.
+     * @param callbackExecutor Executor for executing the provided callbacks.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested USB Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>The accessibility service app must already have approval to access the USB device
+     * from the standard {@link android.hardware.usb.UsbManager} access approval process.
+     *
+     * <p>Note that the callbacks will be executed on the main thread using
+     * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
+     * {@link #connect(UsbDevice, Executor, BrailleDisplayCallback)}.
+     *
+     * @param usbDevice        The Braille display device.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws SecurityException if the caller does not have USB device approval.
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void connect(@NonNull UsbDevice usbDevice,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested USB Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>The accessibility service app must already have approval to access the USB device
+     * from the standard {@link android.hardware.usb.UsbManager} access approval process.
+     *
+     * @param usbDevice        The Braille display device.
+     * @param callbackExecutor Executor for executing the provided callbacks.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws SecurityException if the caller does not have USB device approval.
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void connect(@NonNull UsbDevice usbDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Returns true if a Braille display is currently connected, otherwise false.
+     *
+     * @see #connect
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    boolean isConnected();
+
+    /**
+     * Writes a HID report to the currently connected Braille display.
+     *
+     * <p>This method returns immediately after dispatching the write request to the system.
+     * If the system experiences an error in writing output (e.g. the Braille display is unplugged
+     * after the system receives the write request but before writing the bytes to the Braille
+     * display) then the system will disconnect the Braille display, which calls
+     * {@link BrailleDisplayCallback#onDisconnected()}.
+     *
+     * @param buffer The bytes to write to the Braille display. These bytes should be formatted
+     *               according to the HID report descriptor and the HIDRAW kernel driver.
+     * @throws IOException              if there is no currently connected Braille display.
+     * @throws IllegalArgumentException if the buffer exceeds the maximum safe payload size for
+     *                                  binder transactions of
+     *                                  {@link IBinder#getSuggestedMaxIpcSizeBytes()}
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void write(@NonNull byte[] buffer) throws IOException;
+
+    /**
+     * Disconnects from the currently connected Braille display.
+     *
+     * @see #isConnected()
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void disconnect();
+
+    /**
+     * Provides test Braille display data to be used for automated CTS tests.
+     *
+     * <p>See {@code TEST_BRAILLE_DISPLAY_*} bundle keys.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)
+    @TestApi
+    static void setTestBrailleDisplayData(
+            @NonNull AccessibilityService service,
+            @NonNull List<Bundle> brailleDisplays) {
+        checkApiFlagIsEnabled();
+        final IAccessibilityServiceConnection serviceConnection =
+                AccessibilityInteractionClient.getConnection(service.getConnectionId());
+        if (serviceConnection != null) {
+            try {
+                serviceConnection.setTestBrailleDisplayData(brailleDisplays);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+}
diff --git a/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java
new file mode 100644
index 0000000..cac1dc4
--- /dev/null
+++ b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java
@@ -0,0 +1,267 @@
+/*
+ * 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.accessibilityservice;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.Flags;
+
+import com.android.internal.util.FunctionalUtils;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Default implementation of {@link BrailleDisplayController}.
+ */
+// BrailleDisplayControllerImpl is not an API, but it implements BrailleDisplayController APIs.
+// This @FlaggedApi annotation tells the linter that this method delegates API checks to its
+// callers.
+@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+final class BrailleDisplayControllerImpl implements BrailleDisplayController {
+
+    private final AccessibilityService mAccessibilityService;
+    private final Object mLock;
+
+    private IBrailleDisplayConnection mBrailleDisplayConnection;
+    private Executor mCallbackExecutor;
+    private BrailleDisplayCallback mCallback;
+
+    BrailleDisplayControllerImpl(AccessibilityService accessibilityService,
+            Object lock) {
+        mAccessibilityService = accessibilityService;
+        mLock = lock;
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    public void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull BrailleDisplayCallback callback) {
+        connect(bluetoothDevice, mAccessibilityService.getMainExecutor(), callback);
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    public void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback) {
+        Objects.requireNonNull(bluetoothDevice);
+        Objects.requireNonNull(callbackExecutor);
+        Objects.requireNonNull(callback);
+        connect(serviceConnection -> serviceConnection.connectBluetoothBrailleDisplay(
+                        bluetoothDevice.getAddress(), new IBrailleDisplayControllerWrapper()),
+                callbackExecutor, callback);
+    }
+
+    @Override
+    public void connect(@NonNull UsbDevice usbDevice,
+            @NonNull BrailleDisplayCallback callback) {
+        connect(usbDevice, mAccessibilityService.getMainExecutor(), callback);
+    }
+
+    @Override
+    public void connect(@NonNull UsbDevice usbDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback) {
+        Objects.requireNonNull(usbDevice);
+        Objects.requireNonNull(callbackExecutor);
+        Objects.requireNonNull(callback);
+        connect(serviceConnection -> serviceConnection.connectUsbBrailleDisplay(
+                        usbDevice, new IBrailleDisplayControllerWrapper()),
+                callbackExecutor, callback);
+    }
+
+    /**
+     * Shared implementation for the {@code connect()} API methods.
+     *
+     * <p>Performs a blocking call to system_server to create the connection. Success is
+     * returned through {@link BrailleDisplayCallback#onConnected} while normal connection
+     * errors are returned through {@link BrailleDisplayCallback#onConnectionFailed}. This
+     * connection is implemented using cached data from the HIDRAW driver so it returns
+     * quickly without needing to perform any I/O with the Braille display.
+     *
+     * <p>The AIDL call to system_server is blocking (not posted to a handler thread) so
+     * that runtime exceptions signaling abnormal connection errors from API misuse
+     * (e.g. lacking permissions, providing an invalid BluetoothDevice, calling connect
+     * while already connected) are propagated to the API caller.
+     */
+    private void connect(
+            FunctionalUtils.RemoteExceptionIgnoringConsumer<IAccessibilityServiceConnection>
+                    createConnection,
+            @NonNull Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        if (isConnected()) {
+            throw new IllegalStateException(
+                    "This service already has a connected Braille display");
+        }
+        final IAccessibilityServiceConnection serviceConnection =
+                AccessibilityInteractionClient.getConnection(
+                        mAccessibilityService.getConnectionId());
+        if (serviceConnection == null) {
+            throw new IllegalStateException("Accessibility service is not connected");
+        }
+        synchronized (mLock) {
+            mCallbackExecutor = callbackExecutor;
+            mCallback = callback;
+        }
+        try {
+            createConnection.acceptOrThrow(serviceConnection);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public boolean isConnected() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        return mBrailleDisplayConnection != null;
+    }
+
+    @Override
+    public void write(@NonNull byte[] buffer) throws IOException {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        Objects.requireNonNull(buffer);
+        if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
+            // This same check must be performed in the system to prevent reflection misuse,
+            // but perform it here too to prevent unnecessary IPCs from non-reflection callers.
+            throw new IllegalArgumentException("Invalid write buffer size " + buffer.length);
+        }
+        synchronized (mLock) {
+            if (mBrailleDisplayConnection == null) {
+                throw new IOException("Braille display is not connected");
+            }
+            try {
+                mBrailleDisplayConnection.write(buffer);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            try {
+                if (mBrailleDisplayConnection != null) {
+                    mBrailleDisplayConnection.disconnect();
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } finally {
+                clearConnectionLocked();
+            }
+        }
+    }
+
+    /**
+     * Implementation of the {@code IBrailleDisplayController} AIDL interface provided to
+     * system_server, which system_server uses to pass messages back to this
+     * {@code BrailleDisplayController}.
+     *
+     * <p>Messages from system_server are routed to the {@link BrailleDisplayCallback} callbacks
+     * implemented by the accessibility service.
+     *
+     * <p>Note: Per API Guidelines 7.5 the Binder identity must be cleared before invoking the
+     * callback executor so that Binder identity checks in the callbacks are performed using the
+     * app's identity.
+     */
+    private final class IBrailleDisplayControllerWrapper extends IBrailleDisplayController.Stub {
+        /**
+         * Called when the system successfully connects to a Braille display.
+         */
+        @Override
+        public void onConnected(IBrailleDisplayConnection connection, byte[] hidDescriptor) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mBrailleDisplayConnection = connection;
+                    mCallbackExecutor.execute(() -> mCallback.onConnected(hidDescriptor));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when the system is unable to connect to a Braille display.
+         */
+        @Override
+        public void onConnectionFailed(@BrailleDisplayCallback.ErrorCode int errorCode) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mCallbackExecutor.execute(() -> mCallback.onConnectionFailed(errorCode));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when input is received from the currently connected Braille display.
+         */
+        @Override
+        public void onInput(byte[] input) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    // Ignore input that arrives after disconnection.
+                    if (mBrailleDisplayConnection != null) {
+                        mCallbackExecutor.execute(() -> mCallback.onInput(input));
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when the currently connected Braille display is disconnected.
+         */
+        @Override
+        public void onDisconnected() {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mCallbackExecutor.execute(mCallback::onDisconnected);
+                    clearConnectionLocked();
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    private void clearConnectionLocked() {
+        mBrailleDisplayConnection = null;
+    }
+
+}
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 96716db..dc5c7f6 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -17,10 +17,12 @@
 package android.accessibilityservice;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Bitmap;
 import android.graphics.Region;
+import android.hardware.usb.UsbDevice;
 import android.os.Bundle;
 import android.os.RemoteCallback;
 import android.view.MagnificationSpec;
@@ -160,4 +162,12 @@
     void attachAccessibilityOverlayToDisplay(int interactionId, int displayId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback);
 
     void attachAccessibilityOverlayToWindow(int interactionId, int accessibilityWindowId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+    void connectBluetoothBrailleDisplay(in String bluetoothAddress, in IBrailleDisplayController controller);
+
+    void connectUsbBrailleDisplay(in UsbDevice usbDevice, in IBrailleDisplayController controller);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
+    void setTestBrailleDisplayData(in List<Bundle> brailleDisplays);
 }
\ No newline at end of file
diff --git a/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl
new file mode 100644
index 0000000..ec4d7b1
--- /dev/null
+++ b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl
@@ -0,0 +1,12 @@
+package android.accessibilityservice;
+
+/**
+ * Interface given to a BrailleDisplayController to talk to a BrailleDisplayConnection
+ * in system_server.
+ *
+ * @hide
+ */
+interface IBrailleDisplayConnection {
+    oneway void disconnect();
+    oneway void write(in byte[] output);
+}
\ No newline at end of file
diff --git a/core/java/android/accessibilityservice/IBrailleDisplayController.aidl b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl
new file mode 100644
index 0000000..7a5d83e
--- /dev/null
+++ b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl
@@ -0,0 +1,17 @@
+package android.accessibilityservice;
+
+import android.accessibilityservice.IBrailleDisplayConnection;
+
+/**
+ * Interface given to a BrailleDisplayConnection to talk to a BrailleDisplayController
+ * in an accessibility service.
+ *
+ * IPCs from system_server to apps must be oneway, so designate this entire interface as oneway.
+ * @hide
+ */
+oneway interface IBrailleDisplayController {
+    void onConnected(in IBrailleDisplayConnection connection, in byte[] hidDescriptor);
+    void onConnectionFailed(int error);
+    void onInput(in byte[] input);
+    void onDisconnected();
+}
\ No newline at end of file
diff --git a/core/java/android/provider/ContactKeysManager.java b/core/java/android/provider/ContactKeysManager.java
index bef6456..01aaa3d 100644
--- a/core/java/android/provider/ContactKeysManager.java
+++ b/core/java/android/provider/ContactKeysManager.java
@@ -39,18 +39,19 @@
 import java.util.Objects;
 
 /**
- * ContactKeysManager provides the access to the E2EE contact keys provider.
- * It manages two types of keys - {@link ContactKey} of other users' and the owner's keys -
- * {@link SelfKey}.
+ * ContactKeysManager provides access to the provider of end-to-end encryption contact keys.
+ * It manages two types of keys - {@link ContactKey} and {@link SelfKey}.
  * <ul>
  * <li>
- * For {@link ContactKey} this API allows the insert/update, removal, changing of the
- * verification state, retrieving the keys (either created by or visible to the caller app)
- * operations.
+ * A {@link ContactKey} is a public key associated with a contact. It's used to end-to-end
+ * encrypt the communications between a user and the contact. This API allows operations on
+ * {@link ContactKey}s to insert/update, remove, change the verification state, and retrieving
+ * keys (either created by or visible to the caller app).
  * </li>
  * <li>
- * For {@link SelfKey} this API allows the insert/update, removal, retrieving the self keys
- * (either created by or visible to the caller app) operations.
+ * A {@link SelfKey} is a key for this device, so the key represents the owner of the device.
+ * This API allows operations on {@link SelfKey}s to insert/update, remove, and retrieving
+ * self keys (either created by or visible to the caller app).
  * </li>
  * </ul>
  * Keys are uniquely identified by:
@@ -71,7 +72,7 @@
  * ContactsProvider.
  */
 @FlaggedApi(Flags.FLAG_USER_KEYS)
-public class ContactKeysManager {
+public final class ContactKeysManager {
     /**
      * The authority for the contact keys provider.
      * @hide
@@ -354,9 +355,9 @@
 
 
     private static void validateVerificationState(int verificationState) {
-        if (verificationState != UNVERIFIED
-                && verificationState != VERIFICATION_FAILED
-                && verificationState != VERIFIED) {
+        if (verificationState != VERIFICATION_STATE_UNVERIFIED
+                && verificationState != VERIFICATION_STATE_VERIFICATION_FAILED
+                && verificationState != VERIFICATION_STATE_VERIFIED) {
             throw new IllegalArgumentException("Verification state value "
                     + verificationState + " is not supported");
         }
@@ -600,25 +601,25 @@
      * @hide
      */
     @IntDef(prefix = {"VERIFICATION_STATE_"}, value = {
-            UNVERIFIED,
-            VERIFICATION_FAILED,
-            VERIFIED
+            VERIFICATION_STATE_UNVERIFIED,
+            VERIFICATION_STATE_VERIFICATION_FAILED,
+            VERIFICATION_STATE_VERIFIED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface VerificationState {}
 
     /**
-     * Unverified state of a contact E2EE key.
+     * Unverified state of a contact end to end encrypted key.
      */
-    public static final int UNVERIFIED = 0;
+    public static final int VERIFICATION_STATE_UNVERIFIED = 0;
     /**
-     * Failed verification state of a contact E2EE key.
+     * Failed verification state of a contact end to end encrypted key.
      */
-    public static final int VERIFICATION_FAILED = 1;
+    public static final int VERIFICATION_STATE_VERIFICATION_FAILED = 1;
     /**
-     * Verified state of a contact E2EE key.
+     * Verified state of a contact end to end encrypted key.
      */
-    public static final int VERIFIED = 2;
+    public static final int VERIFICATION_STATE_VERIFIED = 2;
 
     /** @hide */
     public static final class ContactKeys {
@@ -791,7 +792,7 @@
     }
 
     /**
-     * A parcelable class encapsulating other users' E2EE contact key.
+     * A parcelable class encapsulating other users' end to end encrypted contact key.
      */
     public static final class ContactKey implements Parcelable {
         /**
@@ -1056,7 +1057,7 @@
     }
 
     /**
-     * A parcelable class encapsulating self E2EE contact key.
+     * A parcelable class encapsulating self end to end encrypted contact key.
      */
     public static final class SelfKey implements Parcelable {
         /**
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index a6d3bb4..6410609 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -585,9 +585,7 @@
         }
 
         if (ClientFlags.fixLineHeightForLocale()) {
-            if (minimumFontMetrics == null) {
-                paint.getFontMetricsIntForLocale(fm);
-            } else {
+            if (minimumFontMetrics != null) {
                 fm.set(minimumFontMetrics);
                 // Because the font metrics is provided by public APIs, adjust the top/bottom with
                 // ascent/descent: top must be smaller than ascent, bottom must be larger than
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 99bd2ff..5986238 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -767,22 +767,14 @@
         }
 
         int defaultTop;
-        int defaultAscent;
-        int defaultDescent;
+        final int defaultAscent;
+        final int defaultDescent;
         int defaultBottom;
-        if (ClientFlags.fixLineHeightForLocale()) {
-            if (b.mMinimumFontMetrics != null) {
-                defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
-                defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
-                defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
-                defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
-            } else {
-                paint.getFontMetricsIntForLocale(fm);
-                defaultTop = fm.top;
-                defaultAscent = fm.ascent;
-                defaultDescent = fm.descent;
-                defaultBottom = fm.bottom;
-            }
+        if (ClientFlags.fixLineHeightForLocale() && b.mMinimumFontMetrics != null) {
+            defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
+            defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
+            defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
+            defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
 
             // Because the font metrics is provided by public APIs, adjust the top/bottom with
             // ascent/descent: top must be smaller than ascent, bottom must be larger than descent.
@@ -1043,10 +1035,10 @@
 
                     if (endPos < spanEnd) {
                         // preserve metrics for current span
-                        fmTop = fm.top;
-                        fmBottom = fm.bottom;
-                        fmAscent = fm.ascent;
-                        fmDescent = fm.descent;
+                        fmTop = Math.min(defaultTop, fm.top);
+                        fmBottom = Math.max(defaultBottom, fm.bottom);
+                        fmAscent = Math.min(defaultAscent, fm.ascent);
+                        fmDescent = Math.max(defaultDescent, fm.descent);
                     } else {
                         fmTop = fmBottom = fmAscent = fmDescent = 0;
                     }
@@ -1069,7 +1061,7 @@
                 && mLineCount < mMaximumVisibleLineCount) {
             final MeasuredParagraph measuredPara =
                     MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
-            if (ClientFlags.fixLineHeightForLocale()) {
+            if (defaultAscent != 0 && defaultDescent != 0) {
                 fm.top = defaultTop;
                 fm.ascent = defaultAscent;
                 fm.descent = defaultDescent;
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index a9f1897..c22986b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30,6 +30,7 @@
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_UNKNOWN;
 import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH;
 import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE;
+import static android.view.flags.Flags.FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API;
 import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
 import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
 import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout;
@@ -1946,6 +1947,41 @@
     static final int TOOLTIP = 0x40000000;
 
     /** @hide */
+    @IntDef(prefix = { "CONTENT_SENSITIVITY_" }, value = {
+            CONTENT_SENSITIVITY_AUTO,
+            CONTENT_SENSITIVITY_SENSITIVE,
+            CONTENT_SENSITIVITY_NOT_SENSITIVE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ContentSensitivity {}
+
+    /**
+     * Automatically determine whether a view displays sensitive content. For example, available
+     * autofill hints (or some other signal) can be used to determine if this view
+     * displays sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_AUTO = 0x0;
+
+    /**
+     * The view displays sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_SENSITIVE = 0x1;
+
+    /**
+     * The view doesn't display sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 0x2;
+
+    /** @hide */
     @IntDef(flag = true, prefix = { "FOCUSABLES_" }, value = {
             FOCUSABLES_ALL,
             FOCUSABLES_TOUCH_MODE
@@ -3646,6 +3682,7 @@
      *           1                      PFLAG4_ROTARY_HAPTICS_ENABLED
      *          1                       PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT
      *         1                        PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT
+     *       11                         PFLAG4_CONTENT_SENSITIVITY_MASK
      * |-------|-------|-------|-------|
      */
 
@@ -3762,6 +3799,15 @@
      */
     private static final int PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT = 0x800000;
 
+    private static final int PFLAG4_CONTENT_SENSITIVITY_SHIFT = 24;
+
+    /**
+     * Mask for obtaining the bits which specify how to determine whether a view
+     * displays sensitive content or not.
+     */
+    private static final int PFLAG4_CONTENT_SENSITIVITY_MASK =
+            (CONTENT_SENSITIVITY_AUTO | CONTENT_SENSITIVITY_SENSITIVE
+                    | CONTENT_SENSITIVITY_NOT_SENSITIVE) << PFLAG4_CONTENT_SENSITIVITY_SHIFT;
     /* End of masks for mPrivateFlags4 */
 
     /** @hide */
@@ -10150,6 +10196,54 @@
     }
 
     /**
+     * Sets content sensitivity mode to determine whether this view displays sensitive content.
+     *
+     * @param mode {@link #CONTENT_SENSITIVITY_AUTO}, {@link #CONTENT_SENSITIVITY_NOT_SENSITIVE}
+     *                                            or {@link #CONTENT_SENSITIVITY_SENSITIVE}
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public final void setContentSensitivity(@ContentSensitivity int mode)  {
+        mPrivateFlags4 &= ~PFLAG4_CONTENT_SENSITIVITY_MASK;
+        mPrivateFlags4 |= ((mode << PFLAG4_CONTENT_SENSITIVITY_SHIFT)
+                & PFLAG4_CONTENT_SENSITIVITY_MASK);
+    }
+
+    /**
+     * Gets content sensitivity mode to determine whether this view displays sensitive content.
+     *
+     * <p>See {@link #setContentSensitivity(int)} and
+     * {@link #isContentSensitive()} for more info about this mode.
+     *
+     * @return {@link #CONTENT_SENSITIVITY_AUTO} by default, or value passed to
+     * {@link #setContentSensitivity(int)}.
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public @ContentSensitivity
+    final int getContentSensitivity() {
+        return (mPrivateFlags4 & PFLAG4_CONTENT_SENSITIVITY_MASK)
+                >> PFLAG4_CONTENT_SENSITIVITY_SHIFT;
+    }
+
+    /**
+     * Returns whether this view displays sensitive content, based
+     * on the value explicitly set by {@link #setContentSensitivity(int)}.
+     *
+     * @return whether the view displays sensitive content.
+     *
+     * @see #setContentSensitivity(int)
+     * @see #CONTENT_SENSITIVITY_AUTO
+     * @see #CONTENT_SENSITIVITY_SENSITIVE
+     * @see #CONTENT_SENSITIVITY_NOT_SENSITIVE
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public final boolean isContentSensitive() {
+        if (getContentSensitivity() == CONTENT_SENSITIVITY_SENSITIVE) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Gets the mode for determining whether this view is important for content capture.
      *
      * <p>See {@link #setImportantForContentCapture(int)} and
diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java
index aa2474d7..3e0161a 100644
--- a/core/java/android/widget/EditText.java
+++ b/core/java/android/widget/EditText.java
@@ -16,9 +16,13 @@
 
 package android.widget;
 
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.os.Build;
 import android.text.Editable;
 import android.text.Selection;
 import android.text.Spannable;
@@ -29,6 +33,8 @@
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 
+import com.android.internal.R;
+
 /*
  * This is supposed to be a *very* thin veneer over TextView.
  * Do not make any changes here that do anything that a TextView
@@ -85,6 +91,11 @@
     private static final int ID_ITALIC = android.R.id.italic;
     private static final int ID_UNDERLINE = android.R.id.underline;
 
+    /** @hide */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public static final long LINE_HEIGHT_FOR_LOCALE = 303326708L;
+
     public EditText(Context context) {
         this(context, null);
     }
@@ -104,15 +115,39 @@
         final TypedArray a = theme.obtainStyledAttributes(attrs,
                 com.android.internal.R.styleable.EditText, defStyleAttr, defStyleRes);
 
-        final int n = a.getIndexCount();
-        for (int i = 0; i < n; ++i) {
-            int attr = a.getIndex(i);
-            switch (attr) {
-                case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
-                    mStyleShortcutsEnabled = a.getBoolean(attr, false);
-                    break;
+        try {
+            final int n = a.getIndexCount();
+            for (int i = 0; i < n; ++i) {
+                int attr = a.getIndex(i);
+                switch (attr) {
+                    case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
+                        mStyleShortcutsEnabled = a.getBoolean(attr, false);
+                        break;
+                }
             }
+        } finally {
+            a.recycle();
         }
+
+        boolean hasUseLocalePreferredLineHeightForMinimumInt = false;
+        boolean useLocalePreferredLineHeightForMinimumInt = false;
+        TypedArray tvArray = theme.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
+        try {
+            hasUseLocalePreferredLineHeightForMinimumInt =
+                    tvArray.hasValue(R.styleable.TextView_useLocalePreferredLineHeightForMinimum);
+            if (hasUseLocalePreferredLineHeightForMinimumInt) {
+                useLocalePreferredLineHeightForMinimumInt = tvArray.getBoolean(
+                        R.styleable.TextView_useLocalePreferredLineHeightForMinimum, false);
+            }
+        } finally {
+            tvArray.recycle();
+        }
+        if (!hasUseLocalePreferredLineHeightForMinimumInt) {
+            useLocalePreferredLineHeightForMinimumInt =
+                    CompatChanges.isChangeEnabled(LINE_HEIGHT_FOR_LOCALE);
+        }
+        setLocalePreferredLineHeightForMinimumUsed(useLocalePreferredLineHeightForMinimumInt);
     }
 
     @Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 9a4106d9..57e4e6a 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -867,6 +867,8 @@
 
     private boolean mUseBoundsForWidth;
     @Nullable private Paint.FontMetrics mMinimumFontMetrics;
+    @Nullable private Paint.FontMetrics mLocalePreferredFontMetrics;
+    private boolean mUseLocalePreferredLineHeightForMinimum;
 
     @ViewDebug.ExportedProperty(category = "text")
     @UnsupportedAppUsage
@@ -1617,6 +1619,11 @@
                 case com.android.internal.R.styleable.TextView_useBoundsForWidth:
                     mUseBoundsForWidth = a.getBoolean(attr, false);
                     hasUseBoundForWidthValue = true;
+                    break;
+                case com.android.internal.R.styleable
+                        .TextView_useLocalePreferredLineHeightForMinimum:
+                    mUseLocalePreferredLineHeightForMinimum = a.getBoolean(attr, false);
+                    break;
             }
         }
 
@@ -4992,6 +4999,41 @@
     }
 
     /**
+     * Returns true if the locale preferred line height is used for the minimum line height.
+     *
+     * @return true if using locale preferred line height for the minimum line height. Otherwise
+     *         false.
+     *
+     * @see #setLocalePreferredLineHeightForMinimumUsed(boolean)
+     * @see #setMinimumFontMetrics(Paint.FontMetrics)
+     * @see #getMinimumFontMetrics()
+     */
+    @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
+    public boolean isLocalePreferredLineHeightForMinimumUsed() {
+        return mUseLocalePreferredLineHeightForMinimum;
+    }
+
+    /**
+     * Set true if the locale preferred line height is used for the minimum line height.
+     *
+     * By setting this flag to true is equivalenet to call
+     * {@link #setMinimumFontMetrics(Paint.FontMetrics)} with the one obtained by
+     * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}.
+     *
+     * If custom minimum line height was specified by
+     * {@link #setMinimumFontMetrics(Paint.FontMetrics)}, this flag will be ignored.
+     *
+     * @param flag true for using locale preferred line height for the minimum line height.
+     * @see #isLocalePreferredLineHeightForMinimumUsed()
+     * @see #setMinimumFontMetrics(Paint.FontMetrics)
+     * @see #getMinimumFontMetrics()
+     */
+    @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
+    public void setLocalePreferredLineHeightForMinimumUsed(boolean flag) {
+        mUseLocalePreferredLineHeightForMinimum = flag;
+    }
+
+    /**
      * @return whether fallback line spacing is enabled, {@code true} by default
      *
      * @see #setFallbackLineSpacing(boolean)
@@ -10728,6 +10770,21 @@
         return alignment;
     }
 
+    private Paint.FontMetrics getResolvedMinimumFontMetrics() {
+        if (mMinimumFontMetrics != null) {
+            return mMinimumFontMetrics;
+        }
+        if (!mUseLocalePreferredLineHeightForMinimum) {
+            return null;
+        }
+
+        if (mLocalePreferredFontMetrics == null) {
+            mLocalePreferredFontMetrics = new Paint.FontMetrics();
+        }
+        mTextPaint.getFontMetricsForLocale(mLocalePreferredFontMetrics);
+        return mLocalePreferredFontMetrics;
+    }
+
     /**
      * The width passed in is now the desired layout width,
      * not the full view width with padding.
@@ -10792,7 +10849,8 @@
             if (hintBoring == UNKNOWN_BORING) {
                 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
                         isFallbackLineSpacingForBoringLayout(),
-                        mMinimumFontMetrics, mHintBoring);
+                        getResolvedMinimumFontMetrics(), mHintBoring);
+
                 if (hintBoring != null) {
                     mHintBoring = hintBoring;
                 }
@@ -10842,7 +10900,8 @@
                         .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                                 mLineBreakStyle, mLineBreakWordStyle))
                         .setUseBoundsForWidth(mUseBoundsForWidth)
-                        .setMinimumFontMetrics(mMinimumFontMetrics);
+                        .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
+
                 if (shouldEllipsize) {
                     builder.setEllipsize(mEllipsize)
                             .setEllipsizedWidth(ellipsisWidth);
@@ -10907,12 +10966,13 @@
                     .setUseBoundsForWidth(mUseBoundsForWidth)
                     .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
                     .setEllipsizedWidth(ellipsisWidth)
-                    .setMinimumFontMetrics(mMinimumFontMetrics);
+                    .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
             result = builder.build();
         } else {
             if (boring == UNKNOWN_BORING) {
                 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
-                        isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring);
+                        isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
+                        mBoring);
                 if (boring != null) {
                     mBoring = boring;
                 }
@@ -10926,7 +10986,7 @@
                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad, null, wantWidth,
                                 isFallbackLineSpacingForBoringLayout(),
-                                mUseBoundsForWidth, mMinimumFontMetrics);
+                                mUseBoundsForWidth, getResolvedMinimumFontMetrics());
                     } else {
                         result = new BoringLayout(
                                 mTransformed,
@@ -10941,7 +11001,7 @@
                                 null,
                                 boring,
                                 mUseBoundsForWidth,
-                                mMinimumFontMetrics);
+                                getResolvedMinimumFontMetrics());
                     }
 
                     if (useSaved) {
@@ -10953,7 +11013,7 @@
                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad, effectiveEllipsize,
                                 ellipsisWidth, isFallbackLineSpacingForBoringLayout(),
-                                mUseBoundsForWidth, mMinimumFontMetrics);
+                                mUseBoundsForWidth, getResolvedMinimumFontMetrics());
                     } else {
                         result = new BoringLayout(
                                 mTransformed,
@@ -10968,7 +11028,7 @@
                                 effectiveEllipsize,
                                 boring,
                                 mUseBoundsForWidth,
-                                mMinimumFontMetrics);
+                                getResolvedMinimumFontMetrics());
                     }
                 }
             }
@@ -10988,7 +11048,7 @@
                     .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                             mLineBreakStyle, mLineBreakWordStyle))
                     .setUseBoundsForWidth(mUseBoundsForWidth)
-                    .setMinimumFontMetrics(mMinimumFontMetrics);
+                    .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
             if (shouldEllipsize) {
                 builder.setEllipsize(effectiveEllipsize)
                         .setEllipsizedWidth(ellipsisWidth);
@@ -11116,7 +11176,8 @@
 
             if (des < 0) {
                 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
-                        isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring);
+                        isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
+                        mBoring);
                 if (boring != null) {
                     mBoring = boring;
                 }
@@ -11156,7 +11217,7 @@
 
                 if (hintDes < 0) {
                     hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
-                            isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics,
+                            isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
                             mHintBoring);
                     if (hintBoring != null) {
                         mHintBoring = hintBoring;
@@ -11370,7 +11431,7 @@
                 .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                         mLineBreakStyle, mLineBreakWordStyle))
                 .setUseBoundsForWidth(mUseBoundsForWidth)
-                .setMinimumFontMetrics(mMinimumFontMetrics);
+                .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
 
         final StaticLayout layout = layoutBuilder.build();
 
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 45861a3..41bc825 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5857,6 +5857,23 @@
           use glyph bound's as a source of text width.  -->
         <!-- @FlaggedApi("com.android.text.flags.use_bounds_for_width") -->
         <attr name="useBoundsForWidth" format="boolean" />
+        <!-- Whether to use the locale preferred line height for the minimum line height.
+
+          This flag is useful for preventing jitter of entering letters into empty EditText.
+          The line height of the text is determined by the font files used for drawing text in a
+          line. However, in case of the empty text case, the line height cannot be determined and
+          the default line height: usually it is came from a font of Latin script. By making this
+          attribute to true, the TextView/EditText uses a line height that is likely used for the
+          locale associated with the widget. For example, if the system locale is Japanese, the
+          height of the EditText will be adjusted to meet the height of the Japanese font even if
+          the text is empty.
+
+          The default value for EditText is true if targetSdkVersion is
+          {@link android.os.Build.VERSION_CODE#VANILLA_ICE_CREAM} or later, otherwise false.
+          For other TextViews, the default value is false.
+        -->
+        <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") -->
+        <attr name="useLocalePreferredLineHeightForMinimum" format="boolean" />
     </declare-styleable>
     <declare-styleable name="TextViewAppearance">
         <!-- Base text color, typeface, size, and style. -->
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index c14fe57..dbe7196 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -212,6 +212,11 @@
     <integer name="config_emergency_call_wait_for_connection_timeout_millis">20000</integer>
     <java-symbol type="integer" name="config_emergency_call_wait_for_connection_timeout_millis" />
 
+    <!-- Indicates the data limit in bytes that can be used for bootstrap sim until factory reset.
+         -1 means unlimited. -->
+    <integer name="config_esim_bootstrap_data_limit_bytes">-1</integer>
+    <java-symbol type="integer" name="config_esim_bootstrap_data_limit_bytes" />
+
     <!-- Telephony config for the PLMNs of all satellite providers. This is used by satellite modem
          to identify providers that should be ignored if the carrier config
          carrier_supported_satellite_services_per_provider_bundle does not support them.
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 0a6779a9..5ee5555 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -153,6 +153,8 @@
     <public name="requireContentUriPermissionFromCaller" />
     <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") -->
     <public name="languageSettingsActivity"/>
+    <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") -->
+    <public name="useLocalePreferredLineHeightForMinimum"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index 610b8ae..0b0fd66 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -19,10 +19,12 @@
 import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.annotation.NonNull;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Region;
+import android.hardware.usb.UsbDevice;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteCallback;
@@ -237,4 +239,15 @@
             int accessibilityWindowId,
             SurfaceControl sc,
             IAccessibilityInteractionConnectionCallback callback) {}
+
+    @Override
+    public void connectBluetoothBrailleDisplay(String bluetoothAddress,
+            IBrailleDisplayController controller) {}
+
+    @Override
+    public void connectUsbBrailleDisplay(UsbDevice usbDevice,
+            IBrailleDisplayController controller) {}
+
+    @Override
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {}
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e5045ae..70b2f21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2959,13 +2959,17 @@
     }
 
     public void goToFullscreenFromSplit() {
-        boolean leftOrTop;
-        if (mSideStage.isFocused()) {
-            leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+        // If main stage is focused, toEnd = true if
+        // mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT. Otherwise toEnd = false
+        // If side stage is focused, toEnd = true if
+        // mSideStagePosition = SPLIT_POSITION_TOP_OR_LEFT. Otherwise toEnd = false
+        final boolean toEnd;
+        if (mMainStage.isFocused()) {
+            toEnd = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
         } else {
-            leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
+            toEnd = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
         }
-        mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT);
+        mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_FULLSCREEN_SHORTCUT);
     }
 
     /** Move the specified task to fullscreen, regardless of focus state. */
diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java
index 5ed8d40..f1c5c9d 100644
--- a/media/java/android/media/metrics/EditingEndedEvent.java
+++ b/media/java/android/media/metrics/EditingEndedEvent.java
@@ -20,6 +20,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
+import android.annotation.LongDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Bundle;
@@ -27,6 +28,8 @@
 import android.os.Parcelable;
 
 import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
 /** Event for an editing operation having ended. */
@@ -156,14 +159,66 @@
     @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events.
     private final long mTimeSinceCreatedMillis;
 
+    private final ArrayList<MediaItemInfo> mInputMediaItemInfos;
+    @Nullable private final MediaItemInfo mOutputMediaItemInfo;
+
+    /** @hide */
+    @LongDef(
+            prefix = {"OPERATION_TYPE_"},
+            flag = true,
+            value = {
+                OPERATION_TYPE_VIDEO_TRANSCODE,
+                OPERATION_TYPE_AUDIO_TRANSCODE,
+                OPERATION_TYPE_VIDEO_EDIT,
+                OPERATION_TYPE_AUDIO_EDIT,
+                OPERATION_TYPE_VIDEO_TRANSMUX,
+                OPERATION_TYPE_AUDIO_TRANSMUX,
+                OPERATION_TYPE_PAUSED,
+                OPERATION_TYPE_RESUMED,
+            })
+    @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    public @interface OperationType {}
+
+    /** Input video was decoded and re-encoded. */
+    public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1;
+
+    /** Input audio was decoded and re-encoded. */
+    public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 1L << 1;
+
+    /** Input video was edited. */
+    public static final long OPERATION_TYPE_VIDEO_EDIT = 1L << 2;
+
+    /** Input audio was edited. */
+    public static final long OPERATION_TYPE_AUDIO_EDIT = 1L << 3;
+
+    /** Input video samples were writted (muxed) directly to the output file without transcoding. */
+    public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 1L << 4;
+
+    /** Input audio samples were written (muxed) directly to the output file without transcoding. */
+    public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 1L << 5;
+
+    /** The editing operation was paused before it completed. */
+    public static final long OPERATION_TYPE_PAUSED = 1L << 6;
+
+    /** The editing operation resumed a previous (paused) operation. */
+    public static final long OPERATION_TYPE_RESUMED = 1L << 7;
+
+    private final @OperationType long mOperationTypes;
+
     private EditingEndedEvent(
             @FinalState int finalState,
             @ErrorCode int errorCode,
             long timeSinceCreatedMillis,
+            ArrayList<MediaItemInfo> inputMediaItemInfos,
+            @Nullable MediaItemInfo outputMediaItemInfo,
+            @OperationType long operationTypes,
             @NonNull Bundle extras) {
         mFinalState = finalState;
         mErrorCode = errorCode;
         mTimeSinceCreatedMillis = timeSinceCreatedMillis;
+        mInputMediaItemInfos = inputMediaItemInfos;
+        mOutputMediaItemInfo = outputMediaItemInfo;
+        mOperationTypes = operationTypes;
         mMetricsBundle = extras.deepCopy();
     }
 
@@ -194,6 +249,23 @@
         return mTimeSinceCreatedMillis;
     }
 
+    /** Gets information about the input media items, or an empty list if unspecified. */
+    @NonNull
+    public List<MediaItemInfo> getInputMediaItemInfos() {
+        return new ArrayList<>(mInputMediaItemInfos);
+    }
+
+    /** Gets information about the output media item, or {@code null} if unspecified. */
+    @Nullable
+    public MediaItemInfo getOutputMediaItemInfo() {
+        return mOutputMediaItemInfo;
+    }
+
+    /** Gets a set of flags describing the types of operations performed. */
+    public @OperationType long getOperationTypes() {
+        return mOperationTypes;
+    }
+
     /**
      * Gets metrics-related information that is not supported by dedicated methods.
      *
@@ -208,7 +280,7 @@
     @Override
     @NonNull
     public String toString() {
-        return "PlaybackErrorEvent { "
+        return "EditingEndedEvent { "
                 + "finalState = "
                 + mFinalState
                 + ", "
@@ -217,6 +289,15 @@
                 + ", "
                 + "timeSinceCreatedMillis = "
                 + mTimeSinceCreatedMillis
+                + ", "
+                + "inputMediaItemInfos = "
+                + mInputMediaItemInfos
+                + ", "
+                + "outputMediaItemInfo = "
+                + mOutputMediaItemInfo
+                + ", "
+                + "operationTypes = "
+                + mOperationTypes
                 + " }";
     }
 
@@ -227,12 +308,21 @@
         EditingEndedEvent that = (EditingEndedEvent) o;
         return mFinalState == that.mFinalState
                 && mErrorCode == that.mErrorCode
+                && Objects.equals(mInputMediaItemInfos, that.mInputMediaItemInfos)
+                && Objects.equals(mOutputMediaItemInfo, that.mOutputMediaItemInfo)
+                && mOperationTypes == that.mOperationTypes
                 && mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mFinalState, mErrorCode, mTimeSinceCreatedMillis);
+        return Objects.hash(
+                mFinalState,
+                mErrorCode,
+                mInputMediaItemInfos,
+                mOutputMediaItemInfo,
+                mOperationTypes,
+                mTimeSinceCreatedMillis);
     }
 
     @Override
@@ -240,6 +330,9 @@
         dest.writeInt(mFinalState);
         dest.writeInt(mErrorCode);
         dest.writeLong(mTimeSinceCreatedMillis);
+        dest.writeTypedList(mInputMediaItemInfos);
+        dest.writeTypedObject(mOutputMediaItemInfo, /* parcelableFlags= */ 0);
+        dest.writeLong(mOperationTypes);
         dest.writeBundle(mMetricsBundle);
     }
 
@@ -249,15 +342,14 @@
     }
 
     private EditingEndedEvent(@NonNull Parcel in) {
-        int finalState = in.readInt();
-        int errorCode = in.readInt();
-        long timeSinceCreatedMillis = in.readLong();
-        Bundle metricsBundle = in.readBundle();
-
-        mFinalState = finalState;
-        mErrorCode = errorCode;
-        mTimeSinceCreatedMillis = timeSinceCreatedMillis;
-        mMetricsBundle = metricsBundle;
+        mFinalState = in.readInt();
+        mErrorCode = in.readInt();
+        mTimeSinceCreatedMillis = in.readLong();
+        mInputMediaItemInfos = new ArrayList<>();
+        in.readTypedList(mInputMediaItemInfos, MediaItemInfo.CREATOR);
+        mOutputMediaItemInfo = in.readTypedObject(MediaItemInfo.CREATOR);
+        mOperationTypes = in.readLong();
+        mMetricsBundle = in.readBundle();
     }
 
     public static final @NonNull Creator<EditingEndedEvent> CREATOR =
@@ -277,8 +369,11 @@
     @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
     public static final class Builder {
         private final @FinalState int mFinalState;
+        private final ArrayList<MediaItemInfo> mInputMediaItemInfos;
         private @ErrorCode int mErrorCode;
         private long mTimeSinceCreatedMillis;
+        @Nullable private MediaItemInfo mOutputMediaItemInfo;
+        private @OperationType long mOperationTypes;
         private Bundle mMetricsBundle;
 
         /**
@@ -290,6 +385,7 @@
             mFinalState = finalState;
             mErrorCode = ERROR_CODE_NONE;
             mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN;
+            mInputMediaItemInfos = new ArrayList<>();
             mMetricsBundle = new Bundle();
         }
 
@@ -312,20 +408,49 @@
             return this;
         }
 
+        /** Adds information about a media item that was input to the editing operation. */
+        public @NonNull Builder addInputMediaItemInfo(@NonNull MediaItemInfo mediaItemInfo) {
+            mInputMediaItemInfos.add(Objects.requireNonNull(mediaItemInfo));
+            return this;
+        }
+
+        /** Sets information about the output media item. */
+        public @NonNull Builder setOutputMediaItemInfo(@NonNull MediaItemInfo mediaItemInfo) {
+            mOutputMediaItemInfo = Objects.requireNonNull(mediaItemInfo);
+            return this;
+        }
+
+        /**
+         * Adds an operation type to the set of operations performed.
+         *
+         * @param operationType A type of operation performed as part of this editing operation.
+         */
+        public @NonNull Builder addOperationType(@OperationType long operationType) {
+            mOperationTypes |= operationType;
+            return this;
+        }
+
         /**
          * Sets metrics-related information that is not supported by dedicated methods.
          *
          * <p>Used for backwards compatibility by the metrics infrastructure.
          */
         public @NonNull Builder setMetricsBundle(@NonNull Bundle metricsBundle) {
-            mMetricsBundle = metricsBundle;
+            mMetricsBundle = Objects.requireNonNull(metricsBundle);
             return this;
         }
 
         /** Builds an instance. */
         public @NonNull EditingEndedEvent build() {
             return new EditingEndedEvent(
-                    mFinalState, mErrorCode, mTimeSinceCreatedMillis, mMetricsBundle);
+                    mFinalState,
+                    mErrorCode,
+                    mTimeSinceCreatedMillis,
+                    mInputMediaItemInfos,
+                    mOutputMediaItemInfo,
+                    mOperationTypes,
+                    mMetricsBundle);
         }
     }
+
 }
diff --git a/media/java/android/media/metrics/MediaItemInfo.java b/media/java/android/media/metrics/MediaItemInfo.java
new file mode 100644
index 0000000..63dd3cc
--- /dev/null
+++ b/media/java/android/media/metrics/MediaItemInfo.java
@@ -0,0 +1,565 @@
+/*
+ * 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.media.metrics;
+
+import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING;
+
+import android.annotation.FlaggedApi;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.hardware.DataSpace;
+import android.media.MediaCodec;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Size;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** Represents information about a piece of media (for example, an audio or video file). */
+@FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+public final class MediaItemInfo implements Parcelable {
+
+    /** @hide */
+    @IntDef(
+            prefix = {"SOURCE_TYPE_"},
+            value = {
+                SOURCE_TYPE_UNSPECIFIED,
+                SOURCE_TYPE_GALLERY,
+                SOURCE_TYPE_CAMERA,
+                SOURCE_TYPE_EDITING_SESSION,
+                SOURCE_TYPE_LOCAL_FILE,
+                SOURCE_TYPE_REMOTE_FILE,
+                SOURCE_TYPE_REMOTE_LIVE_STREAM,
+                SOURCE_TYPE_GENERATED,
+            })
+    @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    public @interface SourceType {}
+
+    /** The media item's source is not known. */
+    public static final int SOURCE_TYPE_UNSPECIFIED = 0;
+
+    /** The media item came from the device gallery. */
+    public static final int SOURCE_TYPE_GALLERY = 1;
+
+    /** The media item came directly from camera capture. */
+    public static final int SOURCE_TYPE_CAMERA = 2;
+
+    /** The media item was output by a previous editing session. */
+    public static final int SOURCE_TYPE_EDITING_SESSION = 3;
+
+    /** The media item is stored on the local device's file system. */
+    public static final int SOURCE_TYPE_LOCAL_FILE = 4;
+
+    /** The media item is a remote file (for example, it's loaded from an HTTP server). */
+    public static final int SOURCE_TYPE_REMOTE_FILE = 5;
+
+    /** The media item is a remotely-served live stream. */
+    public static final int SOURCE_TYPE_REMOTE_LIVE_STREAM = 6;
+
+    /** The media item was generated by another system. */
+    public static final int SOURCE_TYPE_GENERATED = 7;
+
+    /** @hide */
+    @LongDef(
+            prefix = {"DATA_TYPE_"},
+            flag = true,
+            value = {
+                DATA_TYPE_IMAGE,
+                DATA_TYPE_VIDEO,
+                DATA_TYPE_AUDIO,
+                DATA_TYPE_METADATA,
+                DATA_TYPE_DEPTH,
+                DATA_TYPE_GAIN_MAP,
+                DATA_TYPE_HIGH_FRAME_RATE,
+                DATA_TYPE_CUE_POINTS,
+                DATA_TYPE_GAPLESS,
+                DATA_TYPE_SPATIAL_AUDIO,
+                DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO,
+            })
+    @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    public @interface DataType {}
+
+    /** The media item includes image data. */
+    public static final long DATA_TYPE_IMAGE = 1L;
+
+    /** The media item includes video data. */
+    public static final long DATA_TYPE_VIDEO = 1L << 1;
+
+    /** The media item includes audio data. */
+    public static final long DATA_TYPE_AUDIO = 1L << 2;
+
+    /** The media item includes metadata. */
+    public static final long DATA_TYPE_METADATA = 1L << 3;
+
+    /** The media item includes depth (z-distance) information. */
+    public static final long DATA_TYPE_DEPTH = 1L << 4;
+
+    /** The media item includes gain map information (for example, an Ultra HDR gain map). */
+    public static final long DATA_TYPE_GAIN_MAP = 1L << 5;
+
+    /** The media item includes high frame rate video data. */
+    public static final long DATA_TYPE_HIGH_FRAME_RATE = 1L << 6;
+
+    /** The media item includes time-dependent speed setting metadata. */
+    public static final long DATA_TYPE_CUE_POINTS = 1L << 7;
+
+    /** The media item includes gapless audio metadata. */
+    public static final long DATA_TYPE_GAPLESS = 1L << 8;
+
+    /** The media item includes spatial audio data. */
+    public static final long DATA_TYPE_SPATIAL_AUDIO = 1L << 9;
+
+    /** The media item includes high dynamic range (HDR) video. */
+    public static final long DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO = 1L << 10;
+
+    /** Special value for numerical fields where the value was not specified. */
+    public static final int VALUE_UNSPECIFIED = -1;
+
+    private final @SourceType int mSourceType;
+    private final @DataType long mDataTypes;
+    private final long mDurationMillis;
+    private final long mClipDurationMillis;
+    @Nullable private final String mContainerMimeType;
+    private final List<String> mSampleMimeTypes;
+    private final List<String> mCodecNames;
+    private final int mAudioSampleRateHz;
+    private final int mAudioChannelCount;
+    private final long mAudioSampleCount;
+    private final Size mVideoSize;
+    private final int mVideoDataSpace;
+    private final float mVideoFrameRate;
+    private final long mVideoSampleCount;
+
+    private MediaItemInfo(
+            @SourceType int sourceType,
+            @DataType long dataTypes,
+            long durationMillis,
+            long clipDurationMillis,
+            @Nullable String containerMimeType,
+            List<String> sampleMimeTypes,
+            List<String> codecNames,
+            int audioSampleRateHz,
+            int audioChannelCount,
+            long audioSampleCount,
+            Size videoSize,
+            int videoDataSpace,
+            float videoFrameRate,
+            long videoSampleCount) {
+        mSourceType = sourceType;
+        mDataTypes = dataTypes;
+        mDurationMillis = durationMillis;
+        mClipDurationMillis = clipDurationMillis;
+        mContainerMimeType = containerMimeType;
+        mSampleMimeTypes = sampleMimeTypes;
+        mCodecNames = codecNames;
+        mAudioSampleRateHz = audioSampleRateHz;
+        mAudioChannelCount = audioChannelCount;
+        mAudioSampleCount = audioSampleCount;
+        mVideoSize = videoSize;
+        mVideoDataSpace = videoDataSpace;
+        mVideoFrameRate = videoFrameRate;
+        mVideoSampleCount = videoSampleCount;
+    }
+
+    /**
+     * Returns where the media item came from, or {@link #SOURCE_TYPE_UNSPECIFIED} if not specified.
+     */
+    public @SourceType int getSourceType() {
+        return mSourceType;
+    }
+
+    /** Returns the data types that are present in the media item. */
+    public @DataType long getDataTypes() {
+        return mDataTypes;
+    }
+
+    /**
+     * Returns the duration of the media item, in milliseconds, or {@link #VALUE_UNSPECIFIED} if not
+     * specified.
+     */
+    public long getDurationMillis() {
+        return mDurationMillis;
+    }
+
+    /**
+     * Returns the duration of the clip taken from the media item, in milliseconds, or {@link
+     * #VALUE_UNSPECIFIED} if not specified.
+     */
+    public long getClipDurationMillis() {
+        return mClipDurationMillis;
+    }
+
+    /** Returns the MIME type of the media container, or {@code null} if unspecified. */
+    @Nullable
+    public String getContainerMimeType() {
+        return mContainerMimeType;
+    }
+
+    /**
+     * Returns the MIME types of samples stored in the media container, or an empty list if not
+     * known.
+     */
+    @NonNull
+    public List<String> getSampleMimeTypes() {
+        return new ArrayList<>(mSampleMimeTypes);
+    }
+
+    /**
+     * Returns the {@linkplain MediaCodec#getName() media codec names} for codecs that were used as
+     * part of encoding/decoding this media item, or an empty list if not known or not applicable.
+     */
+    @NonNull
+    public List<String> getCodecNames() {
+        return new ArrayList<>(mCodecNames);
+    }
+
+    /**
+     * Returns the sample rate of audio, in Hertz, or {@link #VALUE_UNSPECIFIED} if not specified.
+     */
+    public int getAudioSampleRateHz() {
+        return mAudioSampleRateHz;
+    }
+
+    /** Returns the number of audio channels, or {@link #VALUE_UNSPECIFIED} if not specified. */
+    public int getAudioChannelCount() {
+        return mAudioChannelCount;
+    }
+
+    /**
+     * Returns the number of audio frames in the item, after clipping (if applicable), or {@link
+     * #VALUE_UNSPECIFIED} if not specified.
+     */
+    public long getAudioSampleCount() {
+        return mAudioSampleCount;
+    }
+
+    /**
+     * Returns the video size, in pixels, or a {@link Size} with width and height set to {@link
+     * #VALUE_UNSPECIFIED} if not specified.
+     */
+    @NonNull
+    public Size getVideoSize() {
+        return mVideoSize;
+    }
+
+    /** Returns the {@linkplain DataSpace data space} for video, as a packed integer. */
+    @SuppressLint("MethodNameUnits") // Packed integer for an android.hardware.DataSpace.
+    public int getVideoDataSpace() {
+        return mVideoDataSpace;
+    }
+
+    /**
+     * Returns the average video frame rate, in frames per second, or {@link #VALUE_UNSPECIFIED} if
+     * not specified.
+     */
+    public float getVideoFrameRate() {
+        return mVideoFrameRate;
+    }
+
+    /**
+     * Returns the number of video frames, aftrer clipping (if applicable), or {@link
+     * #VALUE_UNSPECIFIED} if not specified.
+     */
+    public long getVideoSampleCount() {
+        return mVideoSampleCount;
+    }
+
+    /** Builder for {@link MediaItemInfo}. */
+    @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+    public static final class Builder {
+
+        private @SourceType int mSourceType;
+        private @DataType long mDataTypes;
+        private long mDurationMillis;
+        private long mClipDurationMillis;
+        @Nullable private String mContainerMimeType;
+        private final ArrayList<String> mSampleMimeTypes;
+        private final ArrayList<String> mCodecNames;
+        private int mAudioSampleRateHz;
+        private int mAudioChannelCount;
+        private long mAudioSampleCount;
+        @Nullable private Size mVideoSize;
+        private int mVideoDataSpace;
+        private float mVideoFrameRate;
+        private long mVideoSampleCount;
+
+        /** Creates a new builder. */
+        public Builder() {
+            mSourceType = SOURCE_TYPE_UNSPECIFIED;
+            mDurationMillis = VALUE_UNSPECIFIED;
+            mClipDurationMillis = VALUE_UNSPECIFIED;
+            mSampleMimeTypes = new ArrayList<>();
+            mCodecNames = new ArrayList<>();
+            mAudioSampleRateHz = VALUE_UNSPECIFIED;
+            mAudioChannelCount = VALUE_UNSPECIFIED;
+            mAudioSampleCount = VALUE_UNSPECIFIED;
+            mVideoSize = new Size(VALUE_UNSPECIFIED, VALUE_UNSPECIFIED);
+            mVideoFrameRate = VALUE_UNSPECIFIED;
+            mVideoSampleCount = VALUE_UNSPECIFIED;
+        }
+
+        /** Sets where the media item came from. */
+        public @NonNull Builder setSourceType(@SourceType int sourceType) {
+            mSourceType = sourceType;
+            return this;
+        }
+
+        /** Adds an additional data type represented as part of the media item. */
+        public @NonNull Builder addDataType(@DataType long dataType) {
+            mDataTypes |= dataType;
+            return this;
+        }
+
+        /** Sets the duration of the media item, in milliseconds. */
+        public @NonNull Builder setDurationMillis(long durationMillis) {
+            mDurationMillis = durationMillis;
+            return this;
+        }
+
+        /** Sets the duration of the clip taken from the media item, in milliseconds. */
+        public @NonNull Builder setClipDurationMillis(long clipDurationMillis) {
+            mClipDurationMillis = clipDurationMillis;
+            return this;
+        }
+
+        /** Sets the MIME type of the media container. */
+        public @NonNull Builder setContainerMimeType(@NonNull String containerMimeType) {
+            mContainerMimeType = Objects.requireNonNull(containerMimeType);
+            return this;
+        }
+
+        /** Adds a sample MIME type stored in the media container. */
+        public @NonNull Builder addSampleMimeType(@NonNull String mimeType) {
+            mSampleMimeTypes.add(Objects.requireNonNull(mimeType));
+            return this;
+        }
+
+        /**
+         * Adds an {@linkplain MediaCodec#getName() media codec name} that was used as part of
+         * decoding/encoding this media item.
+         */
+        public @NonNull Builder addCodecName(@NonNull String codecName) {
+            mCodecNames.add(Objects.requireNonNull(codecName));
+            return this;
+        }
+
+        /** Sets the sample rate of audio, in Hertz. */
+        public @NonNull Builder setAudioSampleRateHz(@IntRange(from = 0) int audioSampleRateHz) {
+            mAudioSampleRateHz = audioSampleRateHz;
+            return this;
+        }
+
+        /** Sets the number of audio channels. */
+        public @NonNull Builder setAudioChannelCount(@IntRange(from = 0) int audioChannelCount) {
+            mAudioChannelCount = audioChannelCount;
+            return this;
+        }
+
+        /** Sets the number of audio frames in the item, after clipping (if applicable). */
+        public @NonNull Builder setAudioSampleCount(@IntRange(from = 0) long audioSampleCount) {
+            mAudioSampleCount = audioSampleCount;
+            return this;
+        }
+
+        /** Sets the video size, in pixels. */
+        public @NonNull Builder setVideoSize(@NonNull Size videoSize) {
+            mVideoSize = Objects.requireNonNull(videoSize);
+            return this;
+        }
+
+        /**
+         * Sets the {@link DataSpace} of video frames.
+         *
+         * @param videoDataSpace The data space, returned by {@link DataSpace#pack(int, int, int)}.
+         */
+        public @NonNull Builder setVideoDataSpace(int videoDataSpace) {
+            mVideoDataSpace = videoDataSpace;
+            return this;
+        }
+
+        /** Sets the average video frame rate, in frames per second. */
+        public @NonNull Builder setVideoFrameRate(@FloatRange(from = 0) float videoFrameRate) {
+            mVideoFrameRate = videoFrameRate;
+            return this;
+        }
+
+        /** Sets the number of video frames, after clipping (if applicable). */
+        public @NonNull Builder setVideoSampleCount(@IntRange(from = 0) long videoSampleCount) {
+            mVideoSampleCount = videoSampleCount;
+            return this;
+        }
+
+        /** Builds an instance. */
+        @NonNull
+        public MediaItemInfo build() {
+            return new MediaItemInfo(
+                    mSourceType,
+                    mDataTypes,
+                    mDurationMillis,
+                    mClipDurationMillis,
+                    mContainerMimeType,
+                    mSampleMimeTypes,
+                    mCodecNames,
+                    mAudioSampleRateHz,
+                    mAudioChannelCount,
+                    mAudioSampleCount,
+                    mVideoSize,
+                    mVideoDataSpace,
+                    mVideoFrameRate,
+                    mVideoSampleCount);
+        }
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "MediaItemInfo { "
+                + "sourceType = "
+                + mSourceType
+                + ", "
+                + "dataTypes = "
+                + mDataTypes
+                + ", "
+                + "durationMillis = "
+                + mDurationMillis
+                + ", "
+                + "clipDurationMillis = "
+                + mClipDurationMillis
+                + ", "
+                + "containerMimeType = "
+                + mContainerMimeType
+                + ", "
+                + "sampleMimeTypes = "
+                + mSampleMimeTypes
+                + ", "
+                + "codecNames = "
+                + mCodecNames
+                + ", "
+                + "audioSampleRateHz = "
+                + mAudioSampleRateHz
+                + ", "
+                + "audioChannelCount = "
+                + mAudioChannelCount
+                + ", "
+                + "audioSampleCount = "
+                + mAudioSampleCount
+                + ", "
+                + "videoSize = "
+                + mVideoSize
+                + ", "
+                + "videoDataSpace = "
+                + mVideoDataSpace
+                + ", "
+                + "videoFrameRate = "
+                + mVideoFrameRate
+                + ", "
+                + "videoSampleCount = "
+                + mVideoSampleCount
+                + " }";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        MediaItemInfo that = (MediaItemInfo) o;
+        return mSourceType == that.mSourceType
+                && mDataTypes == that.mDataTypes
+                && mDurationMillis == that.mDurationMillis
+                && mClipDurationMillis == that.mClipDurationMillis
+                && Objects.equals(mContainerMimeType, that.mContainerMimeType)
+                && mSampleMimeTypes.equals(that.mSampleMimeTypes)
+                && mCodecNames.equals(that.mCodecNames)
+                && mAudioSampleRateHz == that.mAudioSampleRateHz
+                && mAudioChannelCount == that.mAudioChannelCount
+                && mAudioSampleCount == that.mAudioSampleCount
+                && Objects.equals(mVideoSize, that.mVideoSize)
+                && Objects.equals(mVideoDataSpace, that.mVideoDataSpace)
+                && mVideoFrameRate == that.mVideoFrameRate
+                && mVideoSampleCount == that.mVideoSampleCount;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mSourceType, mDataTypes);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mSourceType);
+        dest.writeLong(mDataTypes);
+        dest.writeLong(mDurationMillis);
+        dest.writeLong(mClipDurationMillis);
+        dest.writeString(mContainerMimeType);
+        dest.writeStringList(mSampleMimeTypes);
+        dest.writeStringList(mCodecNames);
+        dest.writeInt(mAudioSampleRateHz);
+        dest.writeInt(mAudioChannelCount);
+        dest.writeLong(mAudioSampleCount);
+        dest.writeInt(mVideoSize.getWidth());
+        dest.writeInt(mVideoSize.getHeight());
+        dest.writeInt(mVideoDataSpace);
+        dest.writeFloat(mVideoFrameRate);
+        dest.writeLong(mVideoSampleCount);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    private MediaItemInfo(@NonNull Parcel in) {
+        mSourceType = in.readInt();
+        mDataTypes = in.readLong();
+        mDurationMillis = in.readLong();
+        mClipDurationMillis = in.readLong();
+        mContainerMimeType = in.readString();
+        mSampleMimeTypes = new ArrayList<>();
+        in.readStringList(mSampleMimeTypes);
+        mCodecNames = new ArrayList<>();
+        in.readStringList(mCodecNames);
+        mAudioSampleRateHz = in.readInt();
+        mAudioChannelCount = in.readInt();
+        mAudioSampleCount = in.readLong();
+        int videoSizeWidth = in.readInt();
+        int videoSizeHeight = in.readInt();
+        mVideoSize = new Size(videoSizeWidth, videoSizeHeight);
+        mVideoDataSpace = in.readInt();
+        mVideoFrameRate = in.readFloat();
+        mVideoSampleCount = in.readLong();
+    }
+
+    public static final @NonNull Creator<MediaItemInfo> CREATOR =
+            new Creator<>() {
+                @Override
+                public MediaItemInfo[] newArray(int size) {
+                    return new MediaItemInfo[size];
+                }
+
+                @Override
+                public MediaItemInfo createFromParcel(@NonNull Parcel in) {
+                    return new MediaItemInfo(in);
+                }
+            };
+}
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index bab6781..d622eb8 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -17,3 +17,9 @@
     }
 }
 
+flag {
+   name: "bluetooth_qs_tile_dialog_auto_on_toggle"
+   namespace: "bluetooth"
+   description: "Displays the auto on toggle in the bluetooth QS tile dialog"
+   bug: "316985153"
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index e3012cd..249fa7f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -1621,6 +1621,7 @@
     }
 
     public static class AppEntry extends SizeInfo {
+        @VisibleForTesting String mProfileType;
         @Nullable public final File apkFile;
         public final long id;
         public String label;
@@ -1647,11 +1648,6 @@
          */
         public boolean isHomeApp;
 
-        /**
-         * Whether or not it's a cloned app .
-         */
-        public boolean isCloned;
-
         public String getNormalizedLabel() {
             if (normalizedLabel != null) {
                 return normalizedLabel;
@@ -1692,11 +1688,21 @@
                         () -> this.ensureLabelDescriptionLocked(context));
             }
             UserManager um = UserManager.get(context);
-            this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
             UserInfo userInfo = um.getUserInfo(UserHandle.getUserId(info.uid));
-            if (userInfo != null) {
-                this.isCloned = userInfo.isCloneProfile();
-            }
+            mProfileType = userInfo.userType;
+            this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
+        }
+
+        public boolean isClonedProfile() {
+            return UserManager.USER_TYPE_PROFILE_CLONE.equals(mProfileType);
+        }
+
+        public boolean isManagedProfile() {
+            return UserManager.USER_TYPE_PROFILE_MANAGED.equals(mProfileType);
+        }
+
+        public boolean isPrivateProfile() {
+            return UserManager.USER_TYPE_PROFILE_PRIVATE.equals(mProfileType);
         }
 
         /**
@@ -1890,16 +1896,24 @@
     };
 
     public static final AppFilter FILTER_WORK = new AppFilter() {
-        private int mCurrentUser;
 
         @Override
-        public void init() {
-            mCurrentUser = ActivityManager.getCurrentUser();
-        }
+        public void init() {}
 
         @Override
         public boolean filterApp(AppEntry entry) {
-            return !entry.showInPersonalTab;
+            return !entry.showInPersonalTab && entry.isManagedProfile();
+        }
+    };
+
+    public static final AppFilter FILTER_PRIVATE_PROFILE = new AppFilter() {
+
+        @Override
+        public void init() {}
+
+        @Override
+        public boolean filterApp(AppEntry entry) {
+            return !entry.showInPersonalTab && entry.isPrivateProfile();
         }
     };
 
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
index c5598bf..213a66e 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.pm.ApplicationInfo;
+import android.os.UserManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -297,11 +298,26 @@
     @Test
     public void testPersonalAndWorkFiltersDisplaysCorrectApps() {
         mEntry.showInPersonalTab = true;
+        mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM;
         assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue();
         assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isFalse();
 
         mEntry.showInPersonalTab = false;
+        mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_MANAGED;
         assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse();
         assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isTrue();
     }
+
+    @Test
+    public void testPrivateProfileFilterDisplaysCorrectApps() {
+        mEntry.showInPersonalTab = true;
+        mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM;
+        assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue();
+        assertThat(ApplicationsState.FILTER_PRIVATE_PROFILE.filterApp(mEntry)).isFalse();
+
+        mEntry.showInPersonalTab = false;
+        mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_PRIVATE;
+        assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse();
+        assertThat(ApplicationsState.FILTER_PRIVATE_PROFILE.filterApp(mEntry)).isTrue();
+    }
 }
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7a4e60a..56576f1 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -354,13 +354,6 @@
 }
 
 flag {
-   name: "bluetooth_qs_tile_dialog_auto_on_toggle"
-   namespace: "systemui"
-   description: "Displays the auto on toggle in the bluetooth QS tile dialog"
-   bug: "316985153"
-}
-
-flag {
    name: "smartspace_relocate_to_bottom"
    namespace: "systemui"
    description: "Relocate Smartspace to bottom of the Lock Screen"
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
index a0f916c..ac781ec 100644
--- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -81,7 +81,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="21dp"
-        android:minHeight="145dp"
+        android:minHeight="@dimen/bluetooth_dialog_scroll_view_min_height"
         android:fillViewport="true"
         app:layout_constrainedHeight="true"
         app:layout_constraintStart_toStartOf="parent"
@@ -97,11 +97,11 @@
             <TextView
                 android:id="@+id/bluetooth_toggle_title"
                 android:layout_width="0dp"
-                android:layout_height="64dp"
-                android:maxLines="1"
+                android:layout_height="68dp"
+                android:maxLines="2"
                 android:ellipsize="end"
                 android:gravity="start|center_vertical"
-                android:paddingEnd="0dp"
+                android:paddingEnd="15dp"
                 android:paddingStart="36dp"
                 android:text="@string/turn_on_bluetooth"
                 android:clickable="false"
@@ -114,7 +114,7 @@
             <Switch
                 android:id="@+id/bluetooth_toggle"
                 android:layout_width="wrap_content"
-                android:layout_height="64dp"
+                android:layout_height="68dp"
                 android:gravity="start|center_vertical"
                 android:paddingEnd="40dp"
                 android:contentDescription="@string/turn_on_bluetooth"
@@ -126,14 +126,79 @@
                 app:layout_constraintStart_toEndOf="@+id/bluetooth_toggle_title"
                 app:layout_constraintTop_toTopOf="parent" />
 
+            <androidx.constraintlayout.widget.Group
+                android:id="@+id/bluetooth_auto_on_toggle_layout"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:visibility="gone"
+                app:constraint_referenced_ids="bluetooth_auto_on_toggle_title,bluetooth_auto_on_toggle,bluetooth_auto_on_toggle_info_icon,bluetooth_auto_on_toggle_info_text" />
+
+            <TextView
+                android:id="@+id/bluetooth_auto_on_toggle_title"
+                android:layout_width="0dp"
+                android:layout_height="68dp"
+                android:layout_marginBottom="20dp"
+                android:maxLines="2"
+                android:ellipsize="end"
+                android:text="@string/turn_on_bluetooth_auto_tomorrow"
+                android:gravity="start|center_vertical"
+                android:paddingEnd="15dp"
+                android:paddingStart="36dp"
+                android:clickable="false"
+                android:textAppearance="@style/TextAppearance.Dialog.Title"
+                android:textSize="16sp"
+                app:layout_constraintEnd_toStartOf="@+id/bluetooth_auto_on_toggle"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle_title" />
+
+            <Switch
+                android:id="@+id/bluetooth_auto_on_toggle"
+                android:layout_width="wrap_content"
+                android:layout_height="68dp"
+                android:layout_marginBottom="20dp"
+                android:gravity="start|center_vertical"
+                android:paddingEnd="40dp"
+                android:contentDescription="@string/turn_on_bluetooth_auto_tomorrow"
+                android:switchMinWidth="@dimen/settingslib_switch_track_width"
+                android:theme="@style/MainSwitch.Settingslib"
+                android:thumb="@drawable/settingslib_thumb_selector"
+                android:track="@drawable/settingslib_track_selector"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/bluetooth_auto_on_toggle_title"
+                app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" />
+
+            <ImageView
+                android:id="@+id/bluetooth_auto_on_toggle_info_icon"
+                android:src="@drawable/ic_info_outline"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:tint="?android:attr/textColorTertiary"
+                android:paddingStart="36dp"
+                android:layout_marginTop="20dp"
+                android:layout_marginBottom="@dimen/bluetooth_dialog_layout_margin"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/bluetooth_auto_on_toggle" />
+
+            <TextView
+                android:id="@+id/bluetooth_auto_on_toggle_info_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="20dp"
+                android:paddingStart="36dp"
+                android:paddingEnd="40dp"
+                android:text="@string/turn_on_bluetooth_auto_info"
+                android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/bluetooth_auto_on_toggle_info_icon" />
+
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/device_list"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle"
-                app:layout_constraintBottom_toTopOf="@+id/see_all_button" />
+                app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" />
 
             <Button
                 android:id="@+id/see_all_button"
@@ -168,12 +233,10 @@
                 android:background="@drawable/bluetooth_tile_dialog_bg_off"
                 android:layout_width="0dp"
                 android:layout_height="64dp"
-                android:layout_marginBottom="9dp"
                 android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/see_all_button"
-                app:layout_constraintBottom_toTopOf="@+id/done_button"
                 android:drawableStart="@drawable/ic_add"
                 android:drawablePadding="20dp"
                 android:drawableTint="?android:attr/textColorPrimary"
@@ -186,11 +249,19 @@
                 android:ellipsize="end"
                 android:visibility="gone" />
 
+            <androidx.constraintlayout.widget.Barrier
+                android:id="@+id/barrier"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                app:barrierDirection="bottom"
+                app:constraint_referenced_ids="pair_new_device_button,bluetooth_auto_on_toggle_info_text" />
+
             <Button
                 android:id="@+id/done_button"
                 style="@style/Widget.Dialog.Button"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:layout_marginTop="9dp"
                 android:layout_marginBottom="@dimen/dialog_bottom_padding"
                 android:layout_marginEnd="@dimen/dialog_side_padding"
                 android:layout_marginStart="@dimen/dialog_side_padding"
@@ -200,7 +271,9 @@
                 android:maxLines="1"
                 android:text="@string/inline_done_button"
                 app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintBottom_toBottomOf="parent" />
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/barrier"
+                app:layout_constraintVertical_bias="1" />
         </androidx.constraintlayout.widget.ConstraintLayout>
     </androidx.core.widget.NestedScrollView>
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index cc31754..7537a00 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1717,6 +1717,10 @@
     <dimen name="bluetooth_dialog_layout_margin">16dp</dimen>
     <!-- The height of the bluetooth device in bluetooth dialog. -->
     <dimen name="bluetooth_dialog_device_height">72dp</dimen>
+    <!-- The height of the main scroll view in bluetooth dialog. -->
+    <dimen name="bluetooth_dialog_scroll_view_min_height">145dp</dimen>
+    <!-- The height of the main scroll view in bluetooth dialog with auto on toggle. -->
+    <dimen name="bluetooth_dialog_scroll_view_min_height_with_auto_on">350dp</dimen>
 
     <!-- Height percentage of the parent container occupied by the communal view -->
     <item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 64c6cfa..e401c71 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -669,6 +669,10 @@
     <string name="accessibility_quick_settings_bluetooth_device_tap_to_disconnect">disconnect</string>
     <!-- QuickSettings: Accessibility label to activate a device [CHAR LIMIT=NONE]-->
     <string name="accessibility_quick_settings_bluetooth_device_tap_to_activate">activate</string>
+    <!-- QuickSettings: Bluetooth auto on tomorrow [CHAR LIMIT=NONE]-->
+    <string name="turn_on_bluetooth_auto_tomorrow">Automatically turn on again tomorrow</string>
+    <!-- QuickSettings: Bluetooth auto on info text [CHAR LIMIT=NONE]-->
+    <string name="turn_on_bluetooth_auto_info">Features like Quick Share, Find My Device, and device location use Bluetooth</string>
 
     <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
     <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index d75a72f..75132a5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -24,11 +24,13 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -36,6 +38,7 @@
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 
@@ -52,6 +55,7 @@
     ambientState: AmbientState,
     controller: NotificationStackScrollLayoutController,
     notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
     @Main mainDispatcher: CoroutineDispatcher,
 ) :
     NotificationStackScrollLayoutSection(
@@ -74,12 +78,27 @@
             val bottomMargin =
                 context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
             if (migrateClocksToBlueprint()) {
+                val useLargeScreenHeader =
+                    context.resources.getBoolean(R.bool.config_use_large_screen_shade_header)
+                val marginTopLargeScreen =
+                    if (centralizedStatusBarDimensRefactor()) {
+                        largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
+                    } else {
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.large_screen_shade_header_height
+                        )
+                    }
                 connect(
                     R.id.nssl_placeholder,
                     TOP,
                     R.id.smart_space_barrier_bottom,
                     BOTTOM,
-                    bottomMargin
+                    bottomMargin +
+                        if (useLargeScreenHeader) {
+                            marginTopLargeScreen
+                        } else {
+                            0
+                        }
                 )
             } else {
                 connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index 756a4cc..3e35ae4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -23,13 +23,11 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -37,7 +35,6 @@
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
-import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 
@@ -56,7 +53,6 @@
     notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) :
     NotificationStackScrollLayoutSection(
         context,
@@ -75,16 +71,13 @@
             return
         }
         constraintSet.apply {
-            val splitShadeTopMargin =
-                if (centralizedStatusBarDimensRefactor()) {
-                    largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
-                } else {
-                    context.resources.getDimensionPixelSize(
-                        R.dimen.large_screen_shade_header_height
-                    )
-                }
-            connect(R.id.nssl_placeholder, TOP, PARENT_ID, TOP, splitShadeTopMargin)
-
+            connect(
+                R.id.nssl_placeholder,
+                TOP,
+                PARENT_ID,
+                TOP,
+                context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin)
+            )
             connect(R.id.nssl_placeholder, START, PARENT_ID, START)
             connect(R.id.nssl_placeholder, END, PARENT_ID, END)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
new file mode 100644
index 0000000..dcae088
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */
+@SysUISingleton
+class BluetoothAutoOnInteractor
+@Inject
+constructor(
+    private val bluetoothAutoOnRepository: BluetoothAutoOnRepository,
+) {
+
+    val isEnabled = bluetoothAutoOnRepository.getValue.map { it == ENABLED }.distinctUntilChanged()
+
+    /**
+     * Checks if the auto on value is present in the repository.
+     *
+     * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server).
+     */
+    suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent()
+
+    /**
+     * Sets enabled or disabled based on the provided value.
+     *
+     * @param value `true` to enable the feature, `false` to disable it.
+     */
+    suspend fun setEnabled(value: Boolean) {
+        if (!isValuePresent()) {
+            Log.e(TAG, "Trying to set toggle value while feature not available.")
+        } else {
+            val newValue = if (value) ENABLED else DISABLED
+            bluetoothAutoOnRepository.setValue(newValue)
+        }
+    }
+
+    companion object {
+        private const val TAG = "BluetoothAutoOnInteractor"
+        const val DISABLED = 0
+        const val ENABLED = 1
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
new file mode 100644
index 0000000..e17b4d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.os.UserHandle
+import android.util.Log
+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.repository.UserRepository
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.withContext
+
+/** Repository class responsible for managing the Bluetooth Auto-On feature settings. */
+// TODO(b/316822488): Handle multi-user
+@SysUISingleton
+class BluetoothAutoOnRepository
+@Inject
+constructor(
+    private val secureSettings: SecureSettings,
+    private val userRepository: UserRepository,
+    @Application private val coroutineScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+    // Flow representing the auto on setting value
+    internal val getValue: Flow<Int> =
+        secureSettings
+            .observerFlow(UserHandle.USER_SYSTEM, SETTING_NAME)
+            .onStart { emit(Unit) }
+            .map {
+                if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+                    Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+                    return@map UNSET
+                }
+                secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM)
+            }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
+
+    /**
+     * Checks if the auto on setting value is ever set for the current user.
+     *
+     * @return `true` if the setting value is not UNSET, `false` otherwise.
+     */
+    suspend fun isValuePresent(): Boolean =
+        withContext(backgroundDispatcher) {
+            if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+                Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+                false
+            } else {
+                secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM) != UNSET
+            }
+        }
+
+    /**
+     * Sets the Bluetooth Auto-On setting value for the current user.
+     *
+     * @param value The new setting value to be applied.
+     */
+    suspend fun setValue(value: Int) {
+        withContext(backgroundDispatcher) {
+            if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+                Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+            } else {
+                secureSettings.putIntForUser(SETTING_NAME, value, UserHandle.USER_SYSTEM)
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "BluetoothAutoOnRepository"
+        const val SETTING_NAME = "bluetooth_automatic_turn_on"
+        const val UNSET = -1
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
index 1a06c38..6b53c7a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
@@ -56,7 +56,7 @@
 internal class BluetoothTileDialog
 constructor(
     private val bluetoothToggleInitialValue: Boolean,
-    private val subtitleResIdInitialValue: Int,
+    private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
     private val cachedContentHeight: Int,
     private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
     @Main private val mainDispatcher: CoroutineDispatcher,
@@ -71,6 +71,10 @@
     internal val bluetoothStateToggle
         get() = mutableBluetoothStateToggle.asStateFlow()
 
+    private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+    internal val bluetoothAutoOnToggle
+        get() = mutableBluetoothAutoOnToggle.asStateFlow()
+
     private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> =
         MutableSharedFlow(extraBufferCapacity = 1)
     internal val deviceItemClick
@@ -89,6 +93,8 @@
 
     private lateinit var toggleView: Switch
     private lateinit var subtitleTextView: TextView
+    private lateinit var autoOnToggle: Switch
+    private lateinit var autoOnToggleView: View
     private lateinit var doneButton: View
     private lateinit var seeAllButton: View
     private lateinit var pairNewDeviceButton: View
@@ -108,6 +114,8 @@
 
         toggleView = requireViewById(R.id.bluetooth_toggle)
         subtitleTextView = requireViewById(R.id.bluetooth_tile_dialog_subtitle) as TextView
+        autoOnToggle = requireViewById(R.id.bluetooth_auto_on_toggle)
+        autoOnToggleView = requireViewById(R.id.bluetooth_auto_on_toggle_layout)
         doneButton = requireViewById(R.id.done_button)
         seeAllButton = requireViewById(R.id.see_all_button)
         pairNewDeviceButton = requireViewById(R.id.pair_new_device_button)
@@ -116,7 +124,7 @@
         setupToggle()
         setupRecyclerView()
 
-        subtitleTextView.text = context.getString(subtitleResIdInitialValue)
+        subtitleTextView.text = context.getString(initialUiProperties.subTitleResId)
         doneButton.setOnClickListener { dismiss() }
         seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) }
         pairNewDeviceButton.setOnClickListener {
@@ -124,7 +132,9 @@
         }
         requireViewById<View>(R.id.scroll_view).apply {
             scrollViewContent = this
-            layoutParams.height = cachedContentHeight
+            minimumHeight =
+                resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId)
+            layoutParams.height = maxOf(cachedContentHeight, minimumHeight)
         }
         progressBarAnimation = requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
         progressBarBackground = requireViewById(R.id.bluetooth_tile_dialog_progress_background)
@@ -178,13 +188,27 @@
         }
     }
 
-    internal fun onBluetoothStateUpdated(isEnabled: Boolean, subtitleResId: Int) {
+    internal fun onBluetoothStateUpdated(
+        isEnabled: Boolean,
+        uiProperties: BluetoothTileDialogViewModel.UiProperties
+    ) {
         toggleView.apply {
             isChecked = isEnabled
             setEnabled(true)
             alpha = ENABLED_ALPHA
         }
-        subtitleTextView.text = context.getString(subtitleResId)
+        subtitleTextView.text = context.getString(uiProperties.subTitleResId)
+        autoOnToggleView.visibility = uiProperties.autoOnToggleVisibility
+    }
+
+    internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean) {
+        if (::autoOnToggle.isInitialized) {
+            autoOnToggle.apply {
+                isChecked = isEnabled
+                setEnabled(true)
+                alpha = ENABLED_ALPHA
+            }
+        }
     }
 
     private fun setupToggle() {
@@ -198,6 +222,16 @@
             logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString())
             uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
         }
+
+        autoOnToggleView.visibility = initialUiProperties.autoOnToggleVisibility
+        autoOnToggle.setOnCheckedChangeListener { view, isChecked ->
+            mutableBluetoothAutoOnToggle.value = isChecked
+            view.apply {
+                isEnabled = false
+                alpha = DISABLED_ALPHA
+            }
+            uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
+        }
     }
 
     private fun setupRecyclerView() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
index 86e5dde..cd52e0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
@@ -31,7 +31,8 @@
     @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500),
     @UiEvent(doc = "Active device clicked to disconnect") ACTIVE_DEVICE_DISCONNECT(1507),
     @UiEvent(doc = "Connected other device clicked to disconnect")
-    CONNECTED_OTHER_DEVICE_DISCONNECT(1508);
+    CONNECTED_OTHER_DEVICE_DISCONNECT(1508),
+    @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617);
 
     override fun getId() = metricId
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
index 54bb95c..5a14e5f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -21,9 +21,15 @@
 import android.content.SharedPreferences
 import android.os.Bundle
 import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.view.ViewGroup
+import androidx.annotation.DimenRes
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.flags.Flags.bluetoothQsTileDialogAutoOnToggle
 import com.android.systemui.Prefs
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
@@ -58,6 +64,7 @@
 constructor(
     private val deviceItemInteractor: DeviceItemInteractor,
     private val bluetoothStateInteractor: BluetoothStateInteractor,
+    private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val activityStarter: ActivityStarter,
     private val systemClock: SystemClock,
@@ -143,7 +150,10 @@
                 bluetoothStateInteractor.bluetoothStateUpdate
                     .filterNotNull()
                     .onEach {
-                        dialog.onBluetoothStateUpdated(it, getSubtitleResId(it))
+                        dialog.onBluetoothStateUpdated(
+                            it,
+                            UiProperties.build(it, isAutoOnToggleFeatureAvailable())
+                        )
                         updateDeviceItemJob?.cancel()
                         updateDeviceItemJob = launch {
                             deviceItemInteractor.updateDeviceItems(
@@ -177,6 +187,21 @@
                     }
                     .launchIn(this)
 
+                if (isAutoOnToggleFeatureAvailable()) {
+                    // bluetoothAutoOnUpdate is emitted when bluetooth auto on on/off state is
+                    // changed.
+                    bluetoothAutoOnInteractor.isEnabled
+                        .onEach { dialog.onBluetoothAutoOnUpdated(it) }
+                        .launchIn(this)
+
+                    // bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on
+                    // switch, send the new value to the bluetoothAutoOnInteractor.
+                    dialog.bluetoothAutoOnToggle
+                        .filterNotNull()
+                        .onEach { bluetoothAutoOnInteractor.setEnabled(it) }
+                        .launchIn(this)
+                }
+
                 produce<Unit> { awaitClose { dialog.cancel() } }
             }
     }
@@ -192,7 +217,10 @@
 
         return BluetoothTileDialog(
                 bluetoothStateInteractor.isBluetoothEnabled,
-                getSubtitleResId(bluetoothStateInteractor.isBluetoothEnabled),
+                UiProperties.build(
+                    bluetoothStateInteractor.isBluetoothEnabled,
+                    isAutoOnToggleFeatureAvailable()
+                ),
                 cachedContentHeight,
                 this@BluetoothTileDialogViewModel,
                 mainDispatcher,
@@ -244,6 +272,10 @@
         }
     }
 
+    @VisibleForTesting
+    internal suspend fun isAutoOnToggleFeatureAvailable() =
+        bluetoothQsTileDialogAutoOnToggle() && bluetoothAutoOnInteractor.isValuePresent()
+
     companion object {
         private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog"
         private const val CONTENT_HEIGHT_PREF_KEY = Prefs.Key.BLUETOOTH_TILE_DIALOG_CONTENT_HEIGHT
@@ -251,6 +283,29 @@
             if (isBluetoothEnabled) R.string.quick_settings_bluetooth_tile_subtitle
             else R.string.bt_is_off
     }
+
+    internal data class UiProperties(
+        @StringRes val subTitleResId: Int,
+        val autoOnToggleVisibility: Int,
+        @DimenRes val scrollViewMinHeightResId: Int,
+    ) {
+        companion object {
+            internal fun build(
+                isBluetoothEnabled: Boolean,
+                isAutoOnToggleFeatureAvailable: Boolean
+            ) =
+                UiProperties(
+                    subTitleResId = getSubtitleResId(isBluetoothEnabled),
+                    autoOnToggleVisibility =
+                        if (isAutoOnToggleFeatureAvailable && !isBluetoothEnabled) VISIBLE
+                        else GONE,
+                    scrollViewMinHeightResId =
+                        if (isAutoOnToggleFeatureAvailable)
+                            R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on
+                        else R.dimen.bluetooth_dialog_scroll_view_min_height
+                )
+        }
+    }
 }
 
 internal interface BluetoothTileDialogCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
index b4f578f..ffab9ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
@@ -76,14 +76,10 @@
             }
         val nsslId = R.id.notification_stack_scroller
         constraintSet.apply {
-            connect(nsslId, START, startConstraintId, START)
-            connect(nsslId, END, PARENT_ID, END)
-            connect(nsslId, BOTTOM, PARENT_ID, BOTTOM)
-            connect(nsslId, TOP, PARENT_ID, TOP)
-            setMargin(nsslId, START, marginStart)
-            setMargin(nsslId, END, marginEnd)
-            setMargin(nsslId, TOP, marginTop)
-            setMargin(nsslId, BOTTOM, marginBottom)
+            connect(nsslId, START, startConstraintId, START, marginStart)
+            connect(nsslId, END, PARENT_ID, END, marginEnd)
+            connect(nsslId, BOTTOM, PARENT_ID, BOTTOM, marginBottom)
+            connect(nsslId, TOP, PARENT_ID, TOP, marginTop)
         }
         constraintSet.applyTo(this)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 811da51..e0c2c3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -151,21 +151,20 @@
     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
         interactor.configurationBasedDimensions
             .map {
+                val marginTop =
+                    if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop
                 ConfigurationBasedDimensions(
                     marginStart = if (it.useSplitShade) 0 else it.marginHorizontal,
                     marginEnd = it.marginHorizontal,
                     marginBottom = it.marginBottom,
-                    marginTop =
-                        if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop,
+                    marginTop = marginTop,
                     useSplitShade = it.useSplitShade,
                     paddingTop =
                         if (it.useSplitShade) {
-                            // When in split shade, the margin is applied twice as the legacy shade
-                            // code uses it to calculate padding.
-                            it.keyguardSplitShadeTopMargin - 2 * it.marginTopLargeScreen
+                            marginTop
                         } else {
                             0
-                        }
+                        },
                 )
             }
             .distinctUntilChanged()
@@ -255,13 +254,15 @@
                 isOnLockscreenWithoutShade,
                 keyguardInteractor.notificationContainerBounds,
                 configurationBasedDimensions,
-                interactor.topPosition.sampleCombine(
-                    keyguardTransitionInteractor.isInTransitionToAnyState,
-                    shadeInteractor.qsExpansion,
-                ),
+                interactor.topPosition
+                    .sampleCombine(
+                        keyguardTransitionInteractor.isInTransitionToAnyState,
+                        shadeInteractor.qsExpansion,
+                    )
+                    .onStart { emit(Triple(0f, false, 0f)) }
             ) { onLockscreen, bounds, config, (top, isInTransitionToAnyState, qsExpansion) ->
                 if (onLockscreen) {
-                    bounds.copy(top = bounds.top + config.paddingTop)
+                    bounds.copy(top = bounds.top - config.paddingTop)
                 } else {
                     // When QS expansion > 0, it should directly set the top padding so do not
                     // animate it
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
new file mode 100644
index 0000000..3710713
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.content.pm.UserInfo
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BluetoothAutoOnInteractorTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+    private var secureSettings: FakeSettings = FakeSettings()
+    private val userRepository: FakeUserRepository = FakeUserRepository()
+    private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor
+
+    @Before
+    fun setUp() {
+        bluetoothAutoOnInteractor =
+            BluetoothAutoOnInteractor(
+                BluetoothAutoOnRepository(
+                    secureSettings,
+                    userRepository,
+                    testScope.backgroundScope,
+                    testDispatcher
+                )
+            )
+    }
+
+    @Test
+    fun testSet_bluetoothAutoOnUnset_doNothing() {
+        testScope.runTest {
+            bluetoothAutoOnInteractor.setEnabled(true)
+
+            val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
+
+            runCurrent()
+
+            Truth.assertThat(actualValue).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun testSet_bluetoothAutoOnSet_setNewValue() {
+        testScope.runTest {
+            userRepository.setUserInfos(listOf(SYSTEM_USER))
+            secureSettings.putIntForUser(
+                BluetoothAutoOnRepository.SETTING_NAME,
+                BluetoothAutoOnInteractor.DISABLED,
+                SYSTEM_USER_ID
+            )
+            bluetoothAutoOnInteractor.setEnabled(true)
+
+            val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
+
+            runCurrent()
+
+            Truth.assertThat(actualValue).isEqualTo(true)
+        }
+    }
+
+    companion object {
+        private const val SYSTEM_USER_ID = 0
+        private val SYSTEM_USER =
+            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
new file mode 100644
index 0000000..8986d99
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.DISABLED
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.ENABLED
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.SETTING_NAME
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.UNSET
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BluetoothAutoOnRepositoryTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+    private var secureSettings: FakeSettings = FakeSettings()
+    private val userRepository: FakeUserRepository = FakeUserRepository()
+
+    private lateinit var bluetoothAutoOnRepository: BluetoothAutoOnRepository
+
+    @Before
+    fun setUp() {
+        bluetoothAutoOnRepository =
+            BluetoothAutoOnRepository(
+                secureSettings,
+                userRepository,
+                testScope.backgroundScope,
+                testDispatcher
+            )
+
+        userRepository.setUserInfos(listOf(SECONDARY_USER, SYSTEM_USER))
+    }
+
+    @Test
+    fun testGetValue_valueUnset() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(SYSTEM_USER)
+            val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+            runCurrent()
+
+            assertThat(actualValue).isEqualTo(UNSET)
+            assertThat(bluetoothAutoOnRepository.isValuePresent()).isFalse()
+        }
+    }
+
+    @Test
+    fun testGetValue_valueFalse() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(SYSTEM_USER)
+            val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+            secureSettings.putIntForUser(SETTING_NAME, DISABLED, UserHandle.USER_SYSTEM)
+            runCurrent()
+
+            assertThat(actualValue).isEqualTo(DISABLED)
+        }
+    }
+
+    @Test
+    fun testGetValue_valueTrue() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(SYSTEM_USER)
+            val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+            secureSettings.putIntForUser(SETTING_NAME, ENABLED, UserHandle.USER_SYSTEM)
+            runCurrent()
+
+            assertThat(actualValue).isEqualTo(ENABLED)
+        }
+    }
+
+    @Test
+    fun testGetValue_valueTrue_secondaryUser_returnUnset() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(SECONDARY_USER)
+            val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+            secureSettings.putIntForUser(SETTING_NAME, ENABLED, SECONDARY_USER_ID)
+            runCurrent()
+
+            assertThat(actualValue).isEqualTo(UNSET)
+        }
+    }
+
+    companion object {
+        private const val SYSTEM_USER_ID = 0
+        private const val SECONDARY_USER_ID = 1
+        private val SYSTEM_USER =
+            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+        private val SECONDARY_USER =
+            UserInfo(/* id= */ SECONDARY_USER_ID, /* name= */ "secondary user", /* flags= */ 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
index 154aa1c..70b0417 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
@@ -71,7 +71,11 @@
 
     @Mock private lateinit var logger: BluetoothTileDialogLogger
 
-    private val subtitleResId = R.string.quick_settings_bluetooth_tile_subtitle
+    private val uiProperties =
+        BluetoothTileDialogViewModel.UiProperties.build(
+            isBluetoothEnabled = ENABLED,
+            isAutoOnToggleFeatureAvailable = ENABLED
+        )
 
     private val fakeSystemClock = FakeSystemClock()
 
@@ -90,7 +94,7 @@
         bluetoothTileDialog =
             BluetoothTileDialog(
                 ENABLED,
-                subtitleResId,
+                uiProperties,
                 CONTENT_HEIGHT,
                 bluetoothTileDialogCallback,
                 dispatcher,
@@ -131,7 +135,7 @@
             bluetoothTileDialog =
                 BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     CONTENT_HEIGHT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -166,7 +170,7 @@
         val viewHolder =
             BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     CONTENT_HEIGHT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -194,7 +198,7 @@
         val viewHolder =
             BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     CONTENT_HEIGHT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -219,7 +223,7 @@
             bluetoothTileDialog =
                 BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     CONTENT_HEIGHT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -253,12 +257,36 @@
     }
 
     @Test
-    fun testShowDialog_displayFromCachedHeight() {
+    fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() {
+        testScope.runTest {
+            val cachedHeight = Int.MAX_VALUE
+            bluetoothTileDialog =
+                BluetoothTileDialog(
+                    ENABLED,
+                    uiProperties,
+                    cachedHeight,
+                    bluetoothTileDialogCallback,
+                    dispatcher,
+                    fakeSystemClock,
+                    uiEventLogger,
+                    logger,
+                    mContext
+                )
+            bluetoothTileDialog.show()
+            assertThat(
+                    bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height
+                )
+                .isEqualTo(cachedHeight)
+        }
+    }
+
+    @Test
+    fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() {
         testScope.runTest {
             bluetoothTileDialog =
                 BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     MATCH_PARENT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -271,7 +299,32 @@
             assertThat(
                     bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height
                 )
-                .isEqualTo(MATCH_PARENT)
+                .isGreaterThan(MATCH_PARENT)
+        }
+    }
+
+    @Test
+    fun testShowDialog_bluetoothEnabled_autoOnToggleGone() {
+        testScope.runTest {
+            bluetoothTileDialog =
+                BluetoothTileDialog(
+                    ENABLED,
+                    BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+                    MATCH_PARENT,
+                    bluetoothTileDialogCallback,
+                    dispatcher,
+                    fakeSystemClock,
+                    uiEventLogger,
+                    logger,
+                    mContext
+                )
+            bluetoothTileDialog.show()
+            assertThat(
+                    bluetoothTileDialog
+                        .requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout)
+                        .visibility
+                )
+                .isEqualTo(GONE)
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
index 98ac17b..cb9f4b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -17,20 +17,27 @@
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
 import android.content.SharedPreferences
+import android.content.pm.UserInfo
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.widget.LinearLayout
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.flags.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -84,16 +91,36 @@
     private lateinit var scheduler: TestCoroutineScheduler
     private lateinit var dispatcher: CoroutineDispatcher
     private lateinit var testScope: TestScope
+    private lateinit var secureSettings: FakeSettings
+    private lateinit var userRepository: FakeUserRepository
 
     @Before
     fun setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
         scheduler = TestCoroutineScheduler()
         dispatcher = UnconfinedTestDispatcher(scheduler)
         testScope = TestScope(dispatcher)
+        secureSettings = FakeSettings()
+        userRepository = FakeUserRepository()
+        userRepository.setUserInfos(listOf(SYSTEM_USER))
+        secureSettings.putIntForUser(
+            BluetoothAutoOnRepository.SETTING_NAME,
+            BluetoothAutoOnInteractor.ENABLED,
+            SYSTEM_USER_ID
+        )
         bluetoothTileDialogViewModel =
             BluetoothTileDialogViewModel(
                 deviceItemInteractor,
                 bluetoothStateInteractor,
+                // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor.
+                BluetoothAutoOnInteractor(
+                    BluetoothAutoOnRepository(
+                        secureSettings,
+                        userRepository,
+                        testScope.backgroundScope,
+                        dispatcher
+                    )
+                ),
                 mDialogTransitionAnimator,
                 activityStarter,
                 fakeSystemClock,
@@ -174,4 +201,64 @@
             verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
         }
     }
+
+    @Test
+    fun testBuildUiProperties_bluetoothOn_shouldHideAutoOn() {
+        testScope.runTest {
+            val actual =
+                BluetoothTileDialogViewModel.UiProperties.build(
+                    isBluetoothEnabled = true,
+                    isAutoOnToggleFeatureAvailable = true
+                )
+            assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
+        }
+    }
+
+    @Test
+    fun testBuildUiProperties_bluetoothOff_shouldShowAutoOn() {
+        testScope.runTest {
+            val actual =
+                BluetoothTileDialogViewModel.UiProperties.build(
+                    isBluetoothEnabled = false,
+                    isAutoOnToggleFeatureAvailable = true
+                )
+            assertThat(actual.autoOnToggleVisibility).isEqualTo(VISIBLE)
+        }
+    }
+
+    @Test
+    fun testBuildUiProperties_bluetoothOff_autoOnFeatureUnavailable_shouldHideAutoOn() {
+        testScope.runTest {
+            val actual =
+                BluetoothTileDialogViewModel.UiProperties.build(
+                    isBluetoothEnabled = false,
+                    isAutoOnToggleFeatureAvailable = false
+                )
+            assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
+        }
+    }
+
+    @Test
+    fun testIsAutoOnToggleFeatureAvailable_flagOn_settingValueSet_returnTrue() {
+        testScope.runTest {
+            val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
+            assertThat(actual).isTrue()
+        }
+    }
+
+    @Test
+    fun testIsAutoOnToggleFeatureAvailable_flagOff_settingValueSet_returnFalse() {
+        testScope.runTest {
+            mSetFlagsRule.disableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
+
+            val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
+            assertThat(actual).isFalse()
+        }
+    }
+
+    companion object {
+        private const val SYSTEM_USER_ID = 0
+        private val SYSTEM_USER =
+            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index ff882b1..9055ba4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -138,7 +138,8 @@
 
             configurationRepository.onAnyConfigurationChange()
 
-            assertThat(dimens!!.paddingTop).isEqualTo(30)
+            // Should directly use the header height (flagged off value)
+            assertThat(dimens!!.paddingTop).isEqualTo(10)
         }
 
     @Test
@@ -154,7 +155,8 @@
 
             configurationRepository.onAnyConfigurationChange()
 
-            assertThat(dimens!!.paddingTop).isEqualTo(40)
+            // Should directly use the header height (flagged on value)
+            assertThat(dimens!!.paddingTop).isEqualTo(5)
         }
 
     @Test
@@ -456,8 +458,8 @@
             )
             runCurrent()
 
-            // Top should be equal to bounds (1) + padding adjustment (30)
-            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 31f, bottom = 2f))
+            // Top should be equal to bounds (1) - padding adjustment (10)
+            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = -9f, bottom = 2f))
         }
 
     @Test
@@ -483,8 +485,8 @@
             )
             runCurrent()
 
-            // Top should be equal to bounds (1) + padding adjustment (40)
-            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 41f, bottom = 2f))
+            // Top should be equal to bounds (1) - padding adjustment (5)
+            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = -4f, bottom = 2f))
         }
 
     @Test
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index b64c74e..af47ed2 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -42,10 +42,12 @@
 import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -58,6 +60,7 @@
 import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.usb.UsbDevice;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -2776,4 +2779,23 @@
         t.close();
         mOverlays.clear();
     }
+
+    @Override
+    @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class
+    public void connectBluetoothBrailleDisplay(String bluetoothAddress,
+            IBrailleDisplayController controller) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectUsbBrailleDisplay(UsbDevice usbDevice,
+            IBrailleDisplayController controller) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 5ebe161..b90a66a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -26,16 +26,25 @@
 import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityTrace;
+import android.accessibilityservice.BrailleDisplayController;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.TouchInteractionController;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ParceledListSlice;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -44,6 +53,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Slog;
 import android.view.Display;
 import android.view.MotionEvent;
@@ -55,6 +65,8 @@
 import com.android.server.wm.WindowManagerInternal;
 
 import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -82,6 +94,9 @@
     final Intent mIntent;
     final ActivityTaskManagerInternal mActivityTaskManagerService;
 
+    private BrailleDisplayConnection mBrailleDisplayConnection;
+    private List<Bundle> mTestBrailleDisplays = null;
+
     private final Handler mMainHandler;
 
     private static final class AccessibilityInputMethodSessionCallback
@@ -448,6 +463,16 @@
         }
     }
 
+    @Override
+    public void resetLocked() {
+        super.resetLocked();
+        if (android.view.accessibility.Flags.brailleDisplayHid()) {
+            if (mBrailleDisplayConnection != null) {
+                mBrailleDisplayConnection.disconnect();
+            }
+        }
+    }
+
     public boolean isAccessibilityButtonAvailableLocked(AccessibilityUserState userState) {
         // If the service does not request the accessibility button, it isn't available
         if (!mRequestAccessibilityButton) {
@@ -640,4 +665,123 @@
             }
         }
     }
+
+    private void checkAccessibilityAccessLocked() {
+        if (!hasRightsToCurrentUserLocked()
+                || !mSecurityPolicy.checkAccessibilityAccess(this)) {
+            throw new SecurityException("Caller does not have accessibility access");
+        }
+    }
+
+    /**
+     * Sets up a BrailleDisplayConnection interface for the requested Bluetooth-connected
+     * Braille display.
+     *
+     * @param bluetoothAddress The address from
+     *                         {@link android.bluetooth.BluetoothDevice#getAddress()}.
+     */
+    @Override
+    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+    public void connectBluetoothBrailleDisplay(
+            @NonNull String bluetoothAddress, @NonNull IBrailleDisplayController controller) {
+        if (!android.view.accessibility.Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+        Objects.requireNonNull(bluetoothAddress);
+        Objects.requireNonNull(controller);
+        mContext.enforceCallingPermission(Manifest.permission.BLUETOOTH_CONNECT,
+                "Missing BLUETOOTH_CONNECT permission");
+        if (!BluetoothAdapter.checkBluetoothAddress(bluetoothAddress)) {
+            throw new IllegalArgumentException(
+                    bluetoothAddress + " is not a valid Bluetooth address");
+        }
+        synchronized (mLock) {
+            checkAccessibilityAccessLocked();
+            if (mBrailleDisplayConnection != null) {
+                throw new IllegalStateException(
+                        "This service already has a connected Braille display");
+            }
+            BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this);
+            if (mTestBrailleDisplays != null) {
+                connection.setTestData(mTestBrailleDisplays);
+            }
+            connection.connectLocked(
+                    bluetoothAddress, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
+        }
+    }
+
+    /**
+     * Sets up a BrailleDisplayConnection interface for the requested USB-connected
+     * Braille display.
+     *
+     * <p>The caller package must already have USB permission for this {@link UsbDevice}.
+     */
+    @SuppressLint("MissingPermission") // system_server has the required MANAGE_USB permission
+    @Override
+    @NonNull
+    public void connectUsbBrailleDisplay(@NonNull UsbDevice usbDevice,
+            @NonNull IBrailleDisplayController controller) {
+        if (!android.view.accessibility.Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+        Objects.requireNonNull(usbDevice);
+        Objects.requireNonNull(controller);
+        final UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+        final String usbSerialNumber;
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            if (usbManager == null || !usbManager.hasPermission(
+                    usbDevice, mComponentName.getPackageName(), /*pid=*/ pid, /*uid=*/ uid)) {
+                throw new SecurityException(
+                        "Caller does not have permission to access this UsbDevice");
+            }
+            usbSerialNumber = usbDevice.getSerialNumber();
+            if (TextUtils.isEmpty(usbSerialNumber)) {
+                // If the UsbDevice does not report a serial number for locating the HIDRAW
+                // node then notify connection error ERROR_BRAILLE_DISPLAY_NOT_FOUND.
+                try {
+                    controller.onConnectionFailed(BrailleDisplayController.BrailleDisplayCallback
+                            .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+                } catch (RemoteException e) {
+                    Slog.e(LOG_TAG, "Error calling onConnectionFailed", e);
+                }
+                return;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+        synchronized (mLock) {
+            checkAccessibilityAccessLocked();
+            if (mBrailleDisplayConnection != null) {
+                throw new IllegalStateException(
+                        "This service already has a connected Braille display");
+            }
+            BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this);
+            if (mTestBrailleDisplays != null) {
+                connection.setTestData(mTestBrailleDisplays);
+            }
+            connection.connectLocked(
+                    usbSerialNumber, BrailleDisplayConnection.BUS_USB, controller);
+        }
+    }
+
+    @Override
+    @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {
+        // Enforce that this TestApi is only called by trusted (test) callers.
+        mContext.enforceCallingPermission(Manifest.permission.MANAGE_ACCESSIBILITY,
+                "Missing MANAGE_ACCESSIBILITY permission");
+        mTestBrailleDisplays = brailleDisplays;
+    }
+
+    void onBrailleDisplayConnectedLocked(BrailleDisplayConnection connection) {
+        mBrailleDisplayConnection = connection;
+    }
+
+    // Reset state when the BrailleDisplayConnection object disconnects itself.
+    void onBrailleDisplayDisconnectedLocked() {
+        mBrailleDisplayConnection = null;
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
new file mode 100644
index 0000000..1f18e15
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND;
+import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS;
+
+import android.accessibilityservice.BrailleDisplayController;
+import android.accessibilityservice.IBrailleDisplayConnection;
+import android.accessibilityservice.IBrailleDisplayController;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.PermissionManuallyEnforced;
+import android.annotation.RequiresNoPermission;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * This class represents the connection between {@code system_server} and a connected
+ * Braille Display using the Braille Display HID standard (usage page 0x41).
+ */
+class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
+    private static final String LOG_TAG = "BrailleDisplayConnection";
+
+    /**
+     * Represents the connection type of a Braille display.
+     *
+     * <p>The integer values must match the kernel's bus type values because this bus type is
+     * used to locate the correct HIDRAW node using data from the kernel. These values come
+     * from the UAPI header file bionic/libc/kernel/uapi/linux/input.h, which is guaranteed
+     * to stay constant.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, prefix = {"BUS_"}, value = {
+            BUS_UNKNOWN,
+            BUS_USB,
+            BUS_BLUETOOTH
+    })
+    @interface BusType {
+    }
+    static final int BUS_UNKNOWN = -1;
+    static final int BUS_USB = 0x03;
+    static final int BUS_BLUETOOTH = 0x05;
+
+    // Access to this static object must be guarded by a lock that is shared for all instances
+    // of this class: the singular Accessibility system_server lock (mLock).
+    private static final Set<File> sConnectedNodes = new ArraySet<>();
+
+    // Used to guard to AIDL methods from concurrent calls.
+    // Lock must match the one used by AccessibilityServiceConnection, which itself
+    // comes from AccessibilityManagerService.
+    private final Object mLock;
+    private final AccessibilityServiceConnection mServiceConnection;
+
+
+    private File mHidrawNode;
+    private IBrailleDisplayController mController;
+
+    private Thread mInputThread;
+    private OutputStream mOutputStream;
+    private HandlerThread mOutputThread;
+
+    // mScanner is not final because tests may modify this to use a test-only scanner.
+    private BrailleDisplayScanner mScanner;
+
+    BrailleDisplayConnection(@NonNull Object lock,
+            @NonNull AccessibilityServiceConnection serviceConnection) {
+        this.mLock = Objects.requireNonNull(lock);
+        this.mScanner = getDefaultNativeScanner(getDefaultNativeInterface());
+        this.mServiceConnection = Objects.requireNonNull(serviceConnection);
+    }
+
+    /**
+     * Interface to scan for properties of connected Braille displays.
+     *
+     * <p>Helps simplify testing Braille Display APIs using test data without requiring
+     * a real Braille display to be connected to the device, by using a test implementation
+     * of this interface.
+     *
+     * @see #getDefaultNativeScanner
+     * @see #setTestData
+     */
+    @VisibleForTesting
+    interface BrailleDisplayScanner {
+        Collection<Path> getHidrawNodePaths();
+
+        byte[] getDeviceReportDescriptor(@NonNull Path path);
+
+        String getUniqueId(@NonNull Path path);
+
+        @BusType
+        int getDeviceBusType(@NonNull Path path);
+    }
+
+    /**
+     * Finds the Braille display HIDRAW node associated with the provided unique ID.
+     *
+     * <p>If found, saves instance state for this connection and starts a thread to
+     * read from the Braille display.
+     *
+     * @param expectedUniqueId  The expected unique ID of the device to connect, from
+     *                          {@link UsbDevice#getSerialNumber()}
+     *                          or {@link BluetoothDevice#getAddress()}
+     * @param expectedBusType   The expected bus type from {@link BusType}.
+     * @param controller        Interface containing oneway callbacks used to communicate with the
+     *                          {@link android.accessibilityservice.BrailleDisplayController}.
+     */
+    void connectLocked(
+            @NonNull String expectedUniqueId,
+            @BusType int expectedBusType,
+            @NonNull IBrailleDisplayController controller) {
+        Objects.requireNonNull(expectedUniqueId);
+        this.mController = Objects.requireNonNull(controller);
+
+        final List<Pair<File, byte[]>> result = new ArrayList<>();
+        final Collection<Path> hidrawNodePaths = mScanner.getHidrawNodePaths();
+        if (hidrawNodePaths == null) {
+            Slog.w(LOG_TAG, "Unable to access the HIDRAW node directory");
+            sendConnectionErrorLocked(FLAG_ERROR_CANNOT_ACCESS);
+            return;
+        }
+        boolean unableToGetDescriptor = false;
+        // For every present HIDRAW device node:
+        for (Path path : hidrawNodePaths) {
+            final byte[] descriptor = mScanner.getDeviceReportDescriptor(path);
+            if (descriptor == null) {
+                unableToGetDescriptor = true;
+                continue;
+            }
+            final String uniqueId = mScanner.getUniqueId(path);
+            if (isBrailleDisplay(descriptor)
+                    && mScanner.getDeviceBusType(path) == expectedBusType
+                    && expectedUniqueId.equalsIgnoreCase(uniqueId)) {
+                result.add(Pair.create(path.toFile(), descriptor));
+            }
+        }
+
+        // Return success only when exactly one matching device node is found.
+        if (result.size() != 1) {
+            @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode =
+                    FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND;
+            // If we were unable to get some /dev/hidraw* descriptor then tell the accessibility
+            // service that the device may not have proper access to these device nodes.
+            if (unableToGetDescriptor) {
+                Slog.w(LOG_TAG, "Unable to access some HIDRAW node's descriptor");
+                errorCode |= FLAG_ERROR_CANNOT_ACCESS;
+            } else {
+                Slog.w(LOG_TAG,
+                        "Unable to find a unique Braille display matching the provided device");
+            }
+            sendConnectionErrorLocked(errorCode);
+            return;
+        }
+
+        this.mHidrawNode = result.get(0).first;
+        final byte[] reportDescriptor = result.get(0).second;
+
+        // Only one connection instance should exist for this hidraw node, across
+        // all currently running accessibility services.
+        if (sConnectedNodes.contains(this.mHidrawNode)) {
+            Slog.w(LOG_TAG,
+                    "Unable to find an unused Braille display matching the provided device");
+            sendConnectionErrorLocked(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+            return;
+        }
+        sConnectedNodes.add(this.mHidrawNode);
+
+        startReadingLocked();
+
+        try {
+            mServiceConnection.onBrailleDisplayConnectedLocked(this);
+            mController.onConnected(this, reportDescriptor);
+        } catch (RemoteException e) {
+            Slog.e(LOG_TAG, "Error calling onConnected", e);
+            disconnect();
+        }
+    }
+
+    private void sendConnectionErrorLocked(
+            @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode) {
+        try {
+            mController.onConnectionFailed(errorCode);
+        } catch (RemoteException e) {
+            Slog.e(LOG_TAG, "Error calling onConnectionFailed", e);
+        }
+    }
+
+    /** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */
+    private static boolean isBrailleDisplay(byte[] descriptor) {
+        // TODO: b/316036493 - Check that descriptor includes 0x41 reports.
+        return true;
+    }
+
+    /**
+     * Checks that the AccessibilityService that owns this BrailleDisplayConnection
+     * is still connected to the system.
+     *
+     * @throws IllegalStateException if not connected
+     */
+    private void assertServiceIsConnectedLocked() {
+        if (!mServiceConnection.isConnectedLocked()) {
+            throw new IllegalStateException("Accessibility service is not connected");
+        }
+    }
+
+    /**
+     * Disconnects from this Braille display. This object is no longer valid after
+     * this call returns.
+     */
+    @Override
+    // This is a cleanup method, so allow the call even if the calling service was disabled.
+    @RequiresNoPermission
+    public void disconnect() {
+        synchronized (mLock) {
+            closeInputLocked();
+            closeOutputLocked();
+            mServiceConnection.onBrailleDisplayDisconnectedLocked();
+            try {
+                mController.onDisconnected();
+            } catch (RemoteException e) {
+                Slog.e(LOG_TAG, "Error calling onDisconnected");
+            }
+            sConnectedNodes.remove(this.mHidrawNode);
+        }
+    }
+
+    /**
+     * Writes the provided HID bytes to this Braille display.
+     *
+     * <p>Writes are posted to a background thread handler.
+     *
+     * @param buffer The bytes to write to the Braille display. These bytes should be formatted
+     *               according to the report descriptor.
+     */
+    @Override
+    @PermissionManuallyEnforced // by assertServiceIsConnectedLocked()
+    public void write(@NonNull byte[] buffer) {
+        Objects.requireNonNull(buffer);
+        if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
+            Slog.e(LOG_TAG, "Requested write of size " + buffer.length
+                    + " which is larger than maximum " + IBinder.getSuggestedMaxIpcSizeBytes());
+            return;
+        }
+        synchronized (mLock) {
+            assertServiceIsConnectedLocked();
+            if (mOutputThread == null) {
+                try {
+                    mOutputStream = new FileOutputStream(mHidrawNode);
+                } catch (FileNotFoundException e) {
+                    Slog.e(LOG_TAG, "Unable to create write stream", e);
+                    disconnect();
+                    return;
+                }
+                mOutputThread = new HandlerThread("BrailleDisplayConnection output thread",
+                        Process.THREAD_PRIORITY_BACKGROUND);
+                mOutputThread.setDaemon(true);
+                mOutputThread.start();
+            }
+            // TODO: b/316035785 - Proactively disconnect a misbehaving Braille display by calling
+            //  disconnect() if the mOutputThread handler queue grows too large.
+            mOutputThread.getThreadHandler().post(() -> {
+                try {
+                    mOutputStream.write(buffer);
+                } catch (IOException e) {
+                    Slog.d(LOG_TAG, "Error writing to connected Braille display", e);
+                    disconnect();
+                }
+            });
+        }
+    }
+
+    /**
+     * Starts reading HID bytes from this Braille display.
+     *
+     * <p>Reads are performed on a background thread.
+     */
+    private void startReadingLocked() {
+        mInputThread = new Thread(() -> {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            try (InputStream inputStream = new FileInputStream(mHidrawNode)) {
+                final byte[] buffer = new byte[IBinder.getSuggestedMaxIpcSizeBytes()];
+                int readSize;
+                while (!Thread.interrupted()) {
+                    if (!mHidrawNode.exists()) {
+                        disconnect();
+                        break;
+                    }
+                    // Reading from the HIDRAW character device node will block
+                    // until bytes are available.
+                    readSize = inputStream.read(buffer);
+                    if (readSize > 0) {
+                        try {
+                            // Send the input to the AccessibilityService.
+                            mController.onInput(Arrays.copyOfRange(buffer, 0, readSize));
+                        } catch (RemoteException e) {
+                            // Error communicating with the AccessibilityService.
+                            Slog.e(LOG_TAG, "Error calling onInput", e);
+                            disconnect();
+                            break;
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                Slog.d(LOG_TAG, "Error reading from connected Braille display", e);
+                disconnect();
+            }
+        }, "BrailleDisplayConnection input thread");
+        mInputThread.setDaemon(true);
+        mInputThread.start();
+    }
+
+    /** Stop the Input thread. */
+    private void closeInputLocked() {
+        if (mInputThread != null) {
+            mInputThread.interrupt();
+        }
+        mInputThread = null;
+    }
+
+    /** Stop the Output thread and close the Output stream. */
+    private void closeOutputLocked() {
+        if (mOutputThread != null) {
+            mOutputThread.quit();
+        }
+        mOutputThread = null;
+        if (mOutputStream != null) {
+            try {
+                mOutputStream.close();
+            } catch (IOException e) {
+                Slog.e(LOG_TAG, "Unable to close output stream", e);
+            }
+        }
+        mOutputStream = null;
+    }
+
+    /**
+     * Returns a {@link BrailleDisplayScanner} that opens {@link FileInputStream}s to read
+     * from HIDRAW nodes and perform ioctls using the provided {@link NativeInterface}.
+     */
+    @VisibleForTesting
+    BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) {
+        Objects.requireNonNull(nativeInterface);
+        return new BrailleDisplayScanner() {
+            private static final Path DEVICE_DIR = Path.of("/dev");
+            private static final String HIDRAW_DEVICE_GLOB = "hidraw*";
+
+            @Override
+            public Collection<Path> getHidrawNodePaths() {
+                final List<Path> result = new ArrayList<>();
+                try (DirectoryStream<Path> hidrawNodePaths = Files.newDirectoryStream(
+                        DEVICE_DIR, HIDRAW_DEVICE_GLOB)) {
+                    for (Path path : hidrawNodePaths) {
+                        result.add(path);
+                    }
+                    return result;
+                } catch (IOException e) {
+                    return null;
+                }
+            }
+
+            private <T> T readFromFileDescriptor(Path path, Function<Integer, T> readFn) {
+                try (FileInputStream stream = new FileInputStream(path.toFile())) {
+                    return readFn.apply(stream.getFD().getInt$());
+                } catch (IOException e) {
+                    return null;
+                }
+            }
+
+            @Override
+            public byte[] getDeviceReportDescriptor(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                return readFromFileDescriptor(path, fd -> {
+                    final int descSize = nativeInterface.getHidrawDescSize(fd);
+                    if (descSize > 0) {
+                        return nativeInterface.getHidrawDesc(fd, descSize);
+                    }
+                    return null;
+                });
+            }
+
+            @Override
+            public String getUniqueId(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                return readFromFileDescriptor(path, nativeInterface::getHidrawUniq);
+            }
+
+            @Override
+            public int getDeviceBusType(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                Integer busType = readFromFileDescriptor(path, nativeInterface::getHidrawBusType);
+                return busType != null ? busType : BUS_UNKNOWN;
+            }
+        };
+    }
+
+    /**
+     * Sets test data to be used by CTS tests.
+     *
+     * <p>Replaces the default {@link BrailleDisplayScanner} object for this connection,
+     * and also returns it to allow unit testing this test-only implementation.
+     *
+     * @see BrailleDisplayController#setTestBrailleDisplayData
+     */
+    BrailleDisplayScanner setTestData(@NonNull List<Bundle> brailleDisplays) {
+        Objects.requireNonNull(brailleDisplays);
+        final Map<Path, Bundle> brailleDisplayMap = new ArrayMap<>();
+        for (Bundle brailleDisplay : brailleDisplays) {
+            Path hidrawNodePath = Path.of(brailleDisplay.getString(
+                    BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH));
+            brailleDisplayMap.put(hidrawNodePath, brailleDisplay);
+        }
+        synchronized (mLock) {
+            mScanner = new BrailleDisplayScanner() {
+                @Override
+                public Collection<Path> getHidrawNodePaths() {
+                    return brailleDisplayMap.keySet();
+                }
+
+                @Override
+                public byte[] getDeviceReportDescriptor(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getByteArray(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR);
+                }
+
+                @Override
+                public String getUniqueId(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getString(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID);
+                }
+
+                @Override
+                public int getDeviceBusType(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getBoolean(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH)
+                            ? BUS_BLUETOOTH : BUS_USB;
+                }
+            };
+            return mScanner;
+        }
+    }
+
+    /**
+     * This interface exists to support unit testing {@link #getDefaultNativeScanner}.
+     */
+    @VisibleForTesting
+    interface NativeInterface {
+        int getHidrawDescSize(int fd);
+
+        byte[] getHidrawDesc(int fd, int descSize);
+
+        String getHidrawUniq(int fd);
+
+        int getHidrawBusType(int fd);
+    }
+
+    /** Native interface that actually calls native HIDRAW ioctls. */
+    private NativeInterface getDefaultNativeInterface() {
+        return new NativeInterface() {
+            @Override
+            public int getHidrawDescSize(int fd) {
+                return nativeGetHidrawDescSize(fd);
+            }
+
+            @Override
+            public byte[] getHidrawDesc(int fd, int descSize) {
+                return nativeGetHidrawDesc(fd, descSize);
+            }
+
+            @Override
+            public String getHidrawUniq(int fd) {
+                return nativeGetHidrawUniq(fd);
+            }
+
+            @Override
+            public int getHidrawBusType(int fd) {
+                return nativeGetHidrawBusType(fd);
+            }
+        };
+    }
+
+    private native int nativeGetHidrawDescSize(int fd);
+
+    private native byte[] nativeGetHidrawDesc(int fd, int descSize);
+
+    private native String nativeGetHidrawUniq(int fd);
+
+    private native int nativeGetHidrawBusType(int fd);
+}
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index cec7a79..5d415c2 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -200,7 +200,9 @@
                 Slog.d(TAG, this + ": Starting");
             }
             mRunning = true;
-            updateBinding();
+            if (!Flags.enablePreventionOfKeepAliveRouteProviders()) {
+                updateBinding();
+            }
         }
         if (rebindIfDisconnected && mActiveConnection == null && shouldBind()) {
             unbind();
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
index 233a3ab..fcca94b 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
@@ -150,7 +150,9 @@
                     mCallback.onAddProviderService(proxy);
                 } else if (sourceIndex >= targetIndex) {
                     MediaRoute2ProviderServiceProxy proxy = mProxies.get(sourceIndex);
-                    proxy.start(/* rebindIfDisconnected= */ true); // restart the proxy if needed
+                    proxy.start(
+                            /* rebindIfDisconnected= */
+                                    !Flags.enablePreventionOfKeepAliveRouteProviders());
                     Collections.swap(mProxies, sourceIndex, targetIndex++);
                 }
             }
diff --git a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
index bbe6d3a..2cd8fe0 100644
--- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
+++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
@@ -18,10 +18,13 @@
 
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.hardware.DataSpace;
 import android.media.MediaMetrics;
+import android.media.codec.Enums;
 import android.media.metrics.BundleSession;
 import android.media.metrics.EditingEndedEvent;
 import android.media.metrics.IMediaMetricsManager;
+import android.media.metrics.MediaItemInfo;
 import android.media.metrics.NetworkEvent;
 import android.media.metrics.PlaybackErrorEvent;
 import android.media.metrics.PlaybackMetrics;
@@ -31,7 +34,9 @@
 import android.os.PersistableBundle;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.Properties;
+import android.text.TextUtils;
 import android.util.Base64;
+import android.util.Size;
 import android.util.Slog;
 import android.util.StatsEvent;
 import android.util.StatsLog;
@@ -72,7 +77,14 @@
     private static final String mMetricsId = MediaMetrics.Name.METRICS_MANAGER;
 
     private static final String FAILED_TO_GET = "failed_to_get";
+
+    private static final MediaItemInfo EMPTY_MEDIA_ITEM_INFO = new MediaItemInfo.Builder().build();
+    private static final int DURATION_BUCKETS_BELOW_ONE_MINUTE = 8;
+    private static final int DURATION_BUCKETS_COUNT = 13;
+    private static final String AUDIO_MIME_TYPE_PREFIX = "audio/";
+    private static final String VIDEO_MIME_TYPE_PREFIX = "video/";
     private final SecureRandom mSecureRandom;
+
     @GuardedBy("mLock")
     private Integer mMode = null;
     @GuardedBy("mLock")
@@ -353,6 +365,51 @@
             if (level == LOGGING_LEVEL_BLOCKED) {
                 return;
             }
+            MediaItemInfo inputMediaItemInfo =
+                    event.getInputMediaItemInfos().isEmpty()
+                            ? EMPTY_MEDIA_ITEM_INFO
+                            : event.getInputMediaItemInfos().get(0);
+            String inputAudioSampleMimeType =
+                    getFilteredFirstMimeType(
+                            inputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX);
+            String inputVideoSampleMimeType =
+                    getFilteredFirstMimeType(
+                            inputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX);
+            Size inputVideoSize = inputMediaItemInfo.getVideoSize();
+            int inputVideoResolution = getVideoResolutionEnum(inputVideoSize);
+            if (inputVideoResolution == Enums.RESOLUTION_UNKNOWN) {
+                // Try swapping width/height in case it's a portrait video.
+                inputVideoResolution =
+                        getVideoResolutionEnum(
+                                new Size(inputVideoSize.getHeight(), inputVideoSize.getWidth()));
+            }
+            List<String> inputCodecNames = inputMediaItemInfo.getCodecNames();
+            String inputFirstCodecName = !inputCodecNames.isEmpty() ? inputCodecNames.get(0) : "";
+            String inputSecondCodecName = inputCodecNames.size() > 1 ? inputCodecNames.get(1) : "";
+
+            MediaItemInfo outputMediaItemInfo =
+                    event.getOutputMediaItemInfo() == null
+                            ? EMPTY_MEDIA_ITEM_INFO
+                            : event.getOutputMediaItemInfo();
+            String outputAudioSampleMimeType =
+                    getFilteredFirstMimeType(
+                            outputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX);
+            String outputVideoSampleMimeType =
+                    getFilteredFirstMimeType(
+                            outputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX);
+            Size outputVideoSize = outputMediaItemInfo.getVideoSize();
+            int outputVideoResolution = getVideoResolutionEnum(outputVideoSize);
+            if (outputVideoResolution == Enums.RESOLUTION_UNKNOWN) {
+                // Try swapping width/height in case it's a portrait video.
+                outputVideoResolution =
+                        getVideoResolutionEnum(
+                                new Size(outputVideoSize.getHeight(), outputVideoSize.getWidth()));
+            }
+            List<String> outputCodecNames = outputMediaItemInfo.getCodecNames();
+            String outputFirstCodecName =
+                    !outputCodecNames.isEmpty() ? outputCodecNames.get(0) : "";
+            String outputSecondCodecName =
+                    outputCodecNames.size() > 1 ? outputCodecNames.get(1) : "";
             StatsEvent statsEvent =
                     StatsEvent.newBuilder()
                             .setAtomId(798)
@@ -360,6 +417,66 @@
                             .writeInt(event.getFinalState())
                             .writeInt(event.getErrorCode())
                             .writeLong(event.getTimeSinceCreatedMillis())
+                            .writeInt(getThroughputFps(event))
+                            .writeInt(event.getInputMediaItemInfos().size())
+                            .writeInt(inputMediaItemInfo.getSourceType())
+                            .writeLong(
+                                    getBucketedDurationMillis(
+                                            inputMediaItemInfo.getDurationMillis()))
+                            .writeLong(
+                                    getBucketedDurationMillis(
+                                            inputMediaItemInfo.getClipDurationMillis()))
+                            .writeString(
+                                    getFilteredMimeType(inputMediaItemInfo.getContainerMimeType()))
+                            .writeString(inputAudioSampleMimeType)
+                            .writeString(inputVideoSampleMimeType)
+                            .writeInt(getCodecEnum(inputVideoSampleMimeType))
+                            .writeInt(
+                                    getFilteredAudioSampleRateHz(
+                                            inputMediaItemInfo.getAudioSampleRateHz()))
+                            .writeInt(inputMediaItemInfo.getAudioChannelCount())
+                            .writeInt(inputVideoSize.getWidth())
+                            .writeInt(inputVideoSize.getHeight())
+                            .writeInt(inputVideoResolution)
+                            .writeInt(getVideoResolutionAspectRatioEnum(inputVideoSize))
+                            .writeInt(inputMediaItemInfo.getVideoDataSpace())
+                            .writeInt(
+                                    getVideoHdrFormatEnum(
+                                            inputMediaItemInfo.getVideoDataSpace(),
+                                            inputVideoSampleMimeType))
+                            .writeInt(Math.round(inputMediaItemInfo.getVideoFrameRate()))
+                            .writeInt(getVideoFrameRateEnum(inputMediaItemInfo.getVideoFrameRate()))
+                            .writeString(inputFirstCodecName)
+                            .writeString(inputSecondCodecName)
+                            .writeLong(
+                                    getBucketedDurationMillis(
+                                            outputMediaItemInfo.getDurationMillis()))
+                            .writeLong(
+                                    getBucketedDurationMillis(
+                                            outputMediaItemInfo.getClipDurationMillis()))
+                            .writeString(
+                                    getFilteredMimeType(outputMediaItemInfo.getContainerMimeType()))
+                            .writeString(outputAudioSampleMimeType)
+                            .writeString(outputVideoSampleMimeType)
+                            .writeInt(getCodecEnum(outputVideoSampleMimeType))
+                            .writeInt(
+                                    getFilteredAudioSampleRateHz(
+                                            outputMediaItemInfo.getAudioSampleRateHz()))
+                            .writeInt(outputMediaItemInfo.getAudioChannelCount())
+                            .writeInt(outputVideoSize.getWidth())
+                            .writeInt(outputVideoSize.getHeight())
+                            .writeInt(outputVideoResolution)
+                            .writeInt(getVideoResolutionAspectRatioEnum(outputVideoSize))
+                            .writeInt(outputMediaItemInfo.getVideoDataSpace())
+                            .writeInt(
+                                    getVideoHdrFormatEnum(
+                                            outputMediaItemInfo.getVideoDataSpace(),
+                                            outputVideoSampleMimeType))
+                            .writeInt(Math.round(outputMediaItemInfo.getVideoFrameRate()))
+                            .writeInt(
+                                    getVideoFrameRateEnum(outputMediaItemInfo.getVideoFrameRate()))
+                            .writeString(outputFirstCodecName)
+                            .writeString(outputSecondCodecName)
                             .usePooledBuffer()
                             .build();
             StatsLog.write(statsEvent);
@@ -511,4 +628,215 @@
             }
         }
     }
+
+    private static int getThroughputFps(EditingEndedEvent event) {
+        MediaItemInfo outputMediaItemInfo = event.getOutputMediaItemInfo();
+        if (outputMediaItemInfo == null) {
+            return -1;
+        }
+        long videoSampleCount = outputMediaItemInfo.getVideoSampleCount();
+        if (videoSampleCount == MediaItemInfo.VALUE_UNSPECIFIED) {
+            return -1;
+        }
+        long elapsedTimeMs = event.getTimeSinceCreatedMillis();
+        if (elapsedTimeMs == EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) {
+            return -1;
+        }
+        return (int)
+                Math.min(Integer.MAX_VALUE, Math.round(1000.0 * videoSampleCount / elapsedTimeMs));
+    }
+
+    private static long getBucketedDurationMillis(long durationMillis) {
+        if (durationMillis == MediaItemInfo.VALUE_UNSPECIFIED || durationMillis <= 0) {
+            return -1;
+        }
+        // Bucket values in an exponential distribution to reduce the precision that's stored:
+        // bucket index -> range -> bucketed duration
+        // 1 -> [0, 469 ms) -> 235 ms
+        // 2 -> [469 ms, 938 ms) -> 469 ms
+        // 3 -> [938 ms, 1875 ms) -> 938 ms
+        // 4 -> [1875 ms, 3750 ms) -> 1875 ms
+        // 5 -> [3750 ms, 7500 ms) -> 3750 ms
+        // [...]
+        // 13 -> [960000 ms, max) -> 960000 ms
+        int bucketIndex =
+                (int)
+                        Math.floor(
+                                DURATION_BUCKETS_BELOW_ONE_MINUTE
+                                        + Math.log((durationMillis + 1) / 60_000.0) / Math.log(2));
+        // Clamp to range [0, DURATION_BUCKETS_COUNT].
+        bucketIndex = Math.min(DURATION_BUCKETS_COUNT, Math.max(0, bucketIndex));
+        // Map back onto the representative value for the bucket.
+        return (long)
+                Math.ceil(Math.pow(2, bucketIndex - DURATION_BUCKETS_BELOW_ONE_MINUTE) * 60_000.0);
+    }
+
+    /**
+     * Returns the first entry in {@code mimeTypes} with the given prefix, if it matches the
+     * filtering allowlist. If no entries match the prefix or if the first matching entry is not on
+     * the allowlist, returns an empty string.
+     */
+    private static String getFilteredFirstMimeType(List<String> mimeTypes, String prefix) {
+        int size = mimeTypes.size();
+        for (int i = 0; i < size; i++) {
+            String mimeType = mimeTypes.get(i);
+            if (mimeType.startsWith(prefix)) {
+                return getFilteredMimeType(mimeType);
+            }
+        }
+        return "";
+    }
+
+    private static String getFilteredMimeType(String mimeType) {
+        if (TextUtils.isEmpty(mimeType)) {
+            return "";
+        }
+        // Discard all inputs that aren't allowlisted MIME types.
+        return switch (mimeType) {
+            case "video/mp4",
+                            "video/x-matroska",
+                            "video/webm",
+                            "video/3gpp",
+                            "video/avc",
+                            "video/hevc",
+                            "video/x-vnd.on2.vp8",
+                            "video/x-vnd.on2.vp9",
+                            "video/av01",
+                            "video/mp2t",
+                            "video/mp4v-es",
+                            "video/mpeg",
+                            "video/x-flv",
+                            "video/dolby-vision",
+                            "video/raw",
+                            "audio/mp4",
+                            "audio/mp4a-latm",
+                            "audio/x-matroska",
+                            "audio/webm",
+                            "audio/mpeg",
+                            "audio/mpeg-L1",
+                            "audio/mpeg-L2",
+                            "audio/ac3",
+                            "audio/eac3",
+                            "audio/eac3-joc",
+                            "audio/av4",
+                            "audio/true-hd",
+                            "audio/vnd.dts",
+                            "audio/vnd.dts.hd",
+                            "audio/vorbis",
+                            "audio/opus",
+                            "audio/flac",
+                            "audio/ogg",
+                            "audio/wav",
+                            "audio/midi",
+                            "audio/raw",
+                            "application/mp4",
+                            "application/webm",
+                            "application/x-matroska",
+                            "application/dash+xml",
+                            "application/x-mpegURL",
+                            "application/vnd.ms-sstr+xml" ->
+                    mimeType;
+            default -> "";
+        };
+    }
+
+    private static int getCodecEnum(String mimeType) {
+        if (TextUtils.isEmpty(mimeType)) {
+            return Enums.CODEC_UNKNOWN;
+        }
+        return switch (mimeType) {
+            case "video/avc" -> Enums.CODEC_AVC;
+            case "video/hevc" -> Enums.CODEC_HEVC;
+            case "video/x-vnd.on2.vp8" -> Enums.CODEC_VP8;
+            case "video/x-vnd.on2.vp9" -> Enums.CODEC_VP9;
+            case "video/av01" -> Enums.CODEC_AV1;
+            default -> Enums.CODEC_UNKNOWN;
+        };
+    }
+
+    private static int getFilteredAudioSampleRateHz(int sampleRateHz) {
+        return switch (sampleRateHz) {
+            case 8000, 11025, 16000, 22050, 44100, 48000, 96000, 192000 -> sampleRateHz;
+            default -> -1;
+        };
+    }
+
+    private static int getVideoResolutionEnum(Size size) {
+        int width = size.getWidth();
+        int height = size.getHeight();
+        if (width == 352 && height == 640) {
+            return Enums.RESOLUTION_352X640;
+        } else if (width == 360 && height == 640) {
+            return Enums.RESOLUTION_360X640;
+        } else if (width == 480 && height == 640) {
+            return Enums.RESOLUTION_480X640;
+        } else if (width == 480 && height == 854) {
+            return Enums.RESOLUTION_480X854;
+        } else if (width == 540 && height == 960) {
+            return Enums.RESOLUTION_540X960;
+        } else if (width == 576 && height == 1024) {
+            return Enums.RESOLUTION_576X1024;
+        } else if (width == 1280 && height == 720) {
+            return Enums.RESOLUTION_720P_HD;
+        } else if (width == 1920 && height == 1080) {
+            return Enums.RESOLUTION_1080P_FHD;
+        } else if (width == 1440 && height == 2560) {
+            return Enums.RESOLUTION_1440X2560;
+        } else if (width == 3840 && height == 2160) {
+            return Enums.RESOLUTION_4K_UHD;
+        } else if (width == 7680 && height == 4320) {
+            return Enums.RESOLUTION_8K_UHD;
+        } else {
+            return Enums.RESOLUTION_UNKNOWN;
+        }
+    }
+
+    private static int getVideoResolutionAspectRatioEnum(Size size) {
+        int width = size.getWidth();
+        int height = size.getHeight();
+        if (width <= 0 || height <= 0) {
+            return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_UNSPECIFIED;
+        } else if (width < height) {
+            return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_PORTRAIT;
+        } else if (height < width) {
+            return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_LANDSCAPE;
+        } else {
+            return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_SQUARE;
+        }
+    }
+
+    private static int getVideoHdrFormatEnum(int dataSpace, String mimeType) {
+        if (dataSpace == DataSpace.DATASPACE_UNKNOWN) {
+            return Enums.HDR_FORMAT_UNKNOWN;
+        }
+        if (mimeType.equals("video/dolby-vision")) {
+            return Enums.HDR_FORMAT_DOLBY_VISION;
+        }
+        int standard = DataSpace.getStandard(dataSpace);
+        int transfer = DataSpace.getTransfer(dataSpace);
+        if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_HLG) {
+            return Enums.HDR_FORMAT_HLG;
+        }
+        if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_ST2084) {
+            // We don't currently distinguish HDR10+ from HDR10.
+            return Enums.HDR_FORMAT_HDR10;
+        }
+        return Enums.HDR_FORMAT_NONE;
+    }
+
+    private static int getVideoFrameRateEnum(float frameRate) {
+        int frameRateInt = Math.round(frameRate);
+        return switch (frameRateInt) {
+            case 24 -> Enums.FRAMERATE_24;
+            case 25 -> Enums.FRAMERATE_25;
+            case 30 -> Enums.FRAMERATE_30;
+            case 50 -> Enums.FRAMERATE_50;
+            case 60 -> Enums.FRAMERATE_60;
+            case 120 -> Enums.FRAMERATE_120;
+            case 240 -> Enums.FRAMERATE_240;
+            case 480 -> Enums.FRAMERATE_480;
+            case 960 -> Enums.FRAMERATE_960;
+            default -> Enums.FRAMERATE_UNKNOWN;
+        };
+    }
 }
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index dfa9dce..3607ddd 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -34,6 +34,7 @@
         "tvinput/BufferProducerThread.cpp",
         "tvinput/JTvInputHal.cpp",
         "tvinput/TvInputHal_hidl.cpp",
+        "com_android_server_accessibility_BrailleDisplayConnection.cpp",
         "com_android_server_adb_AdbDebuggingManager.cpp",
         "com_android_server_am_BatteryStatsService.cpp",
         "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp",
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index df7fb99..b999305f 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -15,6 +15,7 @@
 per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS
 per-file com_android_server_Usb* = file:/services/usb/OWNERS
 per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file com_android_server_accessibility_* = file:/services/accessibility/OWNERS
 per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
 per-file com_android_server_lights_* = file:/services/core/java/com/android/server/lights/OWNERS
 per-file com_android_server_location_* = file:/location/java/android/location/OWNERS
diff --git a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
new file mode 100644
index 0000000..9a509a7
--- /dev/null
+++ b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+
+#include <core_jni_helpers.h>
+#include <jni.h>
+#include <linux/hidraw.h>
+#include <linux/input.h>
+#include <nativehelper/JNIHelp.h>
+#include <sys/ioctl.h>
+
+/*
+ * This file defines simple wrappers around the kernel UAPI HIDRAW driver's ioctl() commands.
+ * See kernel example samples/hidraw/hid-example.c
+ *
+ * All methods expect an open file descriptor int from Java.
+ */
+
+namespace android {
+
+namespace {
+
+// Max size we allow for the result from HIDIOCGRAWUNIQ (Bluetooth address or USB serial number).
+// Copied from linux/hid.h struct hid_device->uniq char array size; the ioctl implementation
+// writes at most this many bytes to the provided buffer.
+constexpr int UNIQ_SIZE_MAX = 64;
+
+} // anonymous namespace
+
+static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize(
+        JNIEnv* env, jobject thiz, int fd) {
+    int size = 0;
+    if (ioctl(fd, HIDIOCGRDESCSIZE, &size) < 0) {
+        return -1;
+    }
+    return size;
+}
+
+static jbyteArray com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc(
+        JNIEnv* env, jobject thiz, int fd, int descSize) {
+    struct hidraw_report_descriptor desc;
+    desc.size = descSize;
+    if (ioctl(fd, HIDIOCGRDESC, &desc) < 0) {
+        return nullptr;
+    }
+    jbyteArray result = env->NewByteArray(descSize);
+    if (result != nullptr) {
+        env->SetByteArrayRegion(result, 0, descSize, (jbyte*)desc.value);
+    }
+    // Local ref is not deleted because it is returned to Java
+    return result;
+}
+
+static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq(JNIEnv* env,
+                                                                                       jobject thiz,
+                                                                                       int fd) {
+    char buf[UNIQ_SIZE_MAX];
+    if (ioctl(fd, HIDIOCGRAWUNIQ(UNIQ_SIZE_MAX), buf) < 0) {
+        return nullptr;
+    }
+    // Local ref is not deleted because it is returned to Java
+    return env->NewStringUTF(buf);
+}
+
+static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType(JNIEnv* env,
+                                                                                       jobject thiz,
+                                                                                       int fd) {
+    struct hidraw_devinfo info;
+    if (ioctl(fd, HIDIOCGRAWINFO, &info) < 0) {
+        return -1;
+    }
+    return info.bustype;
+}
+
+static const JNINativeMethod gMethods[] = {
+        {"nativeGetHidrawDescSize", "(I)I",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize},
+        {"nativeGetHidrawDesc", "(II)[B",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc},
+        {"nativeGetHidrawUniq", "(I)Ljava/lang/String;",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq},
+        {"nativeGetHidrawBusType", "(I)I",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType},
+};
+
+int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env) {
+    return RegisterMethodsOrDie(env, "com/android/server/accessibility/BrailleDisplayConnection",
+                                gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 5d1eb49..0936888 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -70,6 +70,7 @@
 int register_com_android_server_display_DisplayControl(JNIEnv* env);
 int register_com_android_server_SystemClockTime(JNIEnv* env);
 int register_android_server_display_smallAreaDetectionController(JNIEnv* env);
+int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env);
 };
 
 using namespace android;
@@ -132,5 +133,6 @@
     register_com_android_server_display_DisplayControl(env);
     register_com_android_server_SystemClockTime(env);
     register_android_server_display_smallAreaDetectionController(env);
+    register_com_android_server_accessibility_BrailleDisplayConnection(env);
     return JNI_VERSION_1_4;
 }
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 28471b3..6bcd778 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
@@ -49,7 +49,6 @@
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONTENT_TRIGGER;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE;
-import static com.android.server.job.controllers.JobStatus.MIN_WINDOW_FOR_FLEXIBILITY_MS;
 import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -410,10 +409,12 @@
 
     @Test
     public void testOnConstantsUpdated_PercentsToDropConstraints() {
+        final long fallbackDuration = 12 * HOUR_IN_MILLIS;
         JobInfo.Builder jb = createJob(0)
-                .setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+                .setOverrideDeadline(HOUR_IN_MILLIS);
         JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+        // Even though the override deadline is 1 hour, the fallback duration is still used.
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 5,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
         setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
                 "500=1|2|3|4"
@@ -441,13 +442,13 @@
                 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,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
         js.setNumDroppedFlexibleConstraints(1);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 2,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 2,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
         js.setNumDroppedFlexibleConstraints(2);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 3,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 3,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
     }
 
@@ -504,37 +505,38 @@
 
     @Test
     public void testGetNextConstraintDropTimeElapsedLocked() {
+        final long fallbackDuration = 50 * HOUR_IN_MILLIS;
         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
+                        + ",300=" + fallbackDuration
                         + ",200=" + 100 * HOUR_IN_MILLIS
                         + ",100=" + 200 * HOUR_IN_MILLIS);
 
         long nextTimeToDropNumConstraints;
 
         // no delay, deadline
-        JobInfo.Builder jb = createJob(0).setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+        JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
         JobStatus js = createJobStatus("time", jb);
 
         assertEquals(JobStatus.NO_EARLIEST_RUNTIME, js.getEarliestRunTime());
-        assertEquals(MIN_WINDOW_FOR_FLEXIBILITY_MS + FROZEN_TIME, js.getLatestRunTimeElapsed());
+        assertEquals(HOUR_IN_MILLIS + FROZEN_TIME, js.getLatestRunTimeElapsed());
         assertEquals(FROZEN_TIME, js.enqueueTime);
 
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 5,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(1);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 6,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(2);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 7,
                 nextTimeToDropNumConstraints);
 
         // delay, no deadline
@@ -574,81 +576,83 @@
 
         // delay, deadline
         jb = createJob(0)
-                .setOverrideDeadline(2 * MIN_WINDOW_FOR_FLEXIBILITY_MS)
-                .setMinimumLatency(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+                .setOverrideDeadline(2 * HOUR_IN_MILLIS)
+                .setMinimumLatency(HOUR_IN_MILLIS);
         js = createJobStatus("time", jb);
 
-        final long windowStart = FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS;
+        final long windowStart = FROZEN_TIME + HOUR_IN_MILLIS;
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+        assertEquals(windowStart + fallbackDuration / 10 * 5,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(1);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6,
+        assertEquals(windowStart + fallbackDuration / 10 * 6,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(2);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7,
+        assertEquals(windowStart + fallbackDuration / 10 * 7,
                 nextTimeToDropNumConstraints);
     }
 
     @Test
     public void testCurPercent() {
+        final long fallbackDuration = 10 * HOUR_IN_MILLIS;
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, "300=" + fallbackDuration);
         long deadline = 100 * MINUTE_IN_MILLIS;
         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,
+        assertEquals(FROZEN_TIME + fallbackDuration,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, FROZEN_TIME));
-        nowElapsed = FROZEN_TIME + 60 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + 6 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
         assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
-        nowElapsed = FROZEN_TIME + 130 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + 13 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
         assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
-        nowElapsed = FROZEN_TIME + 95 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + 9 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
-        assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
+        assertEquals(90, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
         nowElapsed = FROZEN_TIME;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
-        long delay = MINUTE_IN_MILLIS;
-        deadline = 101 * MINUTE_IN_MILLIS;
+        long delay = HOUR_IN_MILLIS;
+        deadline = HOUR_IN_MILLIS + 100 * MINUTE_IN_MILLIS;
         jb = createJob(0).setOverrideDeadline(deadline).setMinimumLatency(delay);
         js = createJobStatus("time", jb);
 
         assertEquals(FROZEN_TIME + delay,
                 mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
-        assertEquals(deadline + FROZEN_TIME,
+        assertEquals(FROZEN_TIME + delay + fallbackDuration,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed,
                         FROZEN_TIME + delay));
 
-        nowElapsed = FROZEN_TIME + delay + 60 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + delay + 6 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
         assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
-        nowElapsed = FROZEN_TIME + 130 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + 13 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
         assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
-        nowElapsed = FROZEN_TIME + delay + 95 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + delay + 9 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
-        assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
+        assertEquals(90, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
     }
 
     @Test
@@ -786,26 +790,27 @@
         // deadline
         JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
         JobStatus js = createJobStatus("time", jb);
-        assertEquals(HOUR_IN_MILLIS + FROZEN_TIME,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+        assertEquals(3 * HOUR_IN_MILLIS + js.enqueueTime,
+                mFlexibilityController
+                        .getLifeCycleEndElapsedLocked(js, nowElapsed, js.enqueueTime));
 
         // no deadline
-        assertEquals(FROZEN_TIME + 2 * HOUR_IN_MILLIS,
+        assertEquals(js.enqueueTime + 2 * HOUR_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_HIGH)),
-                        nowElapsed, 100L));
-        assertEquals(FROZEN_TIME + 3 * HOUR_IN_MILLIS,
+                        nowElapsed, js.enqueueTime));
+        assertEquals(js.enqueueTime + 3 * HOUR_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)),
-                        nowElapsed, 100L));
-        assertEquals(FROZEN_TIME + 4 * HOUR_IN_MILLIS,
+                        nowElapsed, js.enqueueTime));
+        assertEquals(js.enqueueTime + 4 * HOUR_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_LOW)),
-                        nowElapsed, 100L));
-        assertEquals(FROZEN_TIME + 5 * HOUR_IN_MILLIS,
+                        nowElapsed, js.enqueueTime));
+        assertEquals(js.enqueueTime + 5 * HOUR_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_MIN)),
-                        nowElapsed, 100L));
+                        nowElapsed, js.enqueueTime));
     }
 
     @Test
@@ -871,14 +876,16 @@
         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 longDeadlineMs = 14 * 24 * HOUR_IN_MILLIS;
+        JobInfo.Builder jbWithLongDeadline = createJob(0).setOverrideDeadline(longDeadlineMs);
+        JobStatus jsWithLongDeadline = createJobStatus(
+                "testGetLifeCycleEndElapsedLocked_ScoreAddition", jbWithLongDeadline);
+        JobInfo.Builder jbWithShortDeadline =
+                createJob(0).setOverrideDeadline(15 * MINUTE_IN_MILLIS);
+        JobStatus jsWithShortDeadline = createJobStatus(
+                "testGetLifeCycleEndElapsedLocked_ScoreAddition", jbWithShortDeadline);
 
         final long earliestMs = 123L;
-        // no deadline
         assertEquals(earliestMs + HOUR_IN_MILLIS + 5 * 15 * MINUTE_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
@@ -894,6 +901,9 @@
                         createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
                                 createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)),
                         nowElapsed, earliestMs));
+        assertEquals(earliestMs + HOUR_IN_MILLIS + 3 * 15 * MINUTE_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        jsWithShortDeadline, nowElapsed, earliestMs));
         assertEquals(earliestMs + HOUR_IN_MILLIS + 2 * 15 * MINUTE_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
@@ -904,6 +914,9 @@
                         createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
                                 createJob(0).setPriority(JobInfo.PRIORITY_MIN)),
                         nowElapsed, earliestMs));
+        assertEquals(jsWithLongDeadline.enqueueTime + longDeadlineMs,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        jsWithLongDeadline, nowElapsed, earliestMs));
     }
 
     @Test
@@ -1033,8 +1046,8 @@
         JobInfo.Builder jb = createJob(0);
         jb.setMinimumLatency(1);
         jb.setOverrideDeadline(2);
-        JobStatus js = createJobStatus("Disable Flexible When Job Has Short Window", jb);
-        assertFalse(js.hasFlexibilityConstraint());
+        JobStatus js = createJobStatus("testExceptions_ShortWindow", jb);
+        assertTrue(js.hasFlexibilityConstraint());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
index ef80b59..f86cb7b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
@@ -21,9 +21,12 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -31,10 +34,14 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.Manifest;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityTrace;
+import android.accessibilityservice.BrailleDisplayController;
 import android.accessibilityservice.GestureDescription;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IBrailleDisplayConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -43,6 +50,9 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.hardware.display.DisplayManager;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -62,7 +72,9 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
@@ -94,18 +106,30 @@
     AccessibilityServiceInfo mServiceInfo;
     @Mock ResolveInfo mMockResolveInfo;
     @Mock AccessibilitySecurityPolicy mMockSecurityPolicy;
-    @Mock AccessibilityWindowManager mMockA11yWindowManager;
-    @Mock ActivityTaskManagerInternal mMockActivityTaskManagerInternal;
-    @Mock AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
-    @Mock AccessibilityTrace mMockA11yTrace;
-    @Mock WindowManagerInternal mMockWindowManagerInternal;
-    @Mock SystemActionPerformer mMockSystemActionPerformer;
-    @Mock KeyEventDispatcher mMockKeyEventDispatcher;
+    @Mock
+    AccessibilityWindowManager mMockA11yWindowManager;
+    @Mock
+    ActivityTaskManagerInternal mMockActivityTaskManagerInternal;
+    @Mock
+    AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
+    @Mock
+    AccessibilityTrace mMockA11yTrace;
+    @Mock
+    WindowManagerInternal mMockWindowManagerInternal;
+    @Mock
+    SystemActionPerformer mMockSystemActionPerformer;
+    @Mock
+    KeyEventDispatcher mMockKeyEventDispatcher;
     @Mock
     MagnificationProcessor mMockMagnificationProcessor;
-    @Mock IBinder mMockIBinder;
-    @Mock IAccessibilityServiceClient mMockServiceClient;
-    @Mock MotionEventInjector mMockMotionEventInjector;
+    @Mock
+    IBinder mMockIBinder;
+    @Mock
+    IAccessibilityServiceClient mMockServiceClient;
+    @Mock
+    IBrailleDisplayController mMockBrailleDisplayController;
+    @Mock
+    MotionEventInjector mMockMotionEventInjector;
 
     MessageCapturingHandler mHandler = new MessageCapturingHandler(null);
 
@@ -134,6 +158,7 @@
                 mMockWindowManagerInternal, mMockSystemActionPerformer,
                 mMockA11yWindowManager, mMockActivityTaskManagerInternal);
         when(mMockSecurityPolicy.canPerformGestures(mConnection)).thenReturn(true);
+        when(mMockSecurityPolicy.checkAccessibilityAccess(mConnection)).thenReturn(true);
     }
 
     @After
@@ -291,6 +316,119 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay() throws Exception {
+        final String macAddress = "00:11:22:33:AA:BB";
+        final byte[] descriptor = {0x05, 0x41};
+        Bundle bd = new Bundle();
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null");
+        bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor);
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, macAddress);
+        bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, true);
+        mConnection.setTestBrailleDisplayData(List.of(bd));
+
+        mConnection.connectBluetoothBrailleDisplay(macAddress, mMockBrailleDisplayController);
+
+        ArgumentCaptor<IBrailleDisplayConnection> connection =
+                ArgumentCaptor.forClass(IBrailleDisplayConnection.class);
+        verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor));
+        // Cleanup the connection.
+        connection.getValue().disconnect();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForMissingBluetoothConnectPermission() {
+        doThrow(SecurityException.class).when(mMockContext)
+                .enforceCallingPermission(eq(Manifest.permission.BLUETOOTH_CONNECT), any());
+
+        assertThrows(SecurityException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay("unused",
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForNullMacAddress() {
+        assertThrows(NullPointerException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay(null,
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForMisformattedMacAddress() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay("12:34",
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay() throws Exception {
+        final String serialNumber = "myUsbDevice";
+        final byte[] descriptor = {0x05, 0x41};
+        Bundle bd = new Bundle();
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null");
+        bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor);
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, serialNumber);
+        bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, false);
+        mConnection.setTestBrailleDisplayData(List.of(bd));
+        UsbDevice usbDevice = Mockito.mock(UsbDevice.class);
+        when(usbDevice.getSerialNumber()).thenReturn(serialNumber);
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(eq(usbDevice), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(true);
+
+        mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController);
+
+        ArgumentCaptor<IBrailleDisplayConnection> connection =
+                ArgumentCaptor.forClass(IBrailleDisplayConnection.class);
+        verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor));
+        // Cleanup the connection.
+        connection.getValue().disconnect();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_throwsForMissingUsbPermission() {
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(false);
+
+        assertThrows(SecurityException.class,
+                () -> mConnection.connectUsbBrailleDisplay(Mockito.mock(UsbDevice.class),
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_throwsForNullDevice() {
+        assertThrows(NullPointerException.class,
+                () -> mConnection.connectUsbBrailleDisplay(null, mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_callsOnConnectionFailedForEmptySerialNumber()
+            throws Exception {
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(true);
+        UsbDevice usbDevice = Mockito.mock(UsbDevice.class);
+        when(usbDevice.getSerialNumber()).thenReturn("");
+
+        mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController);
+
+        verify(mMockBrailleDisplayController).onConnectionFailed(
+                BrailleDisplayController.BrailleDisplayCallback
+                        .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_RESETTABLE_DYNAMIC_PROPERTIES)
     public void binderDied_resetA11yServiceInfo() {
         final int flag = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
new file mode 100644
index 0000000..7c278ce
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.accessibilityservice.BrailleDisplayController;
+import android.os.Bundle;
+import android.testing.DexmakerShareClassLoaderRule;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Tests for internal details of {@link BrailleDisplayConnection}.
+ *
+ * <p>Prefer adding new tests in CTS where possible.
+ */
+public class BrailleDisplayConnectionTest {
+    private static final Path NULL_PATH = Path.of("/dev/null");
+
+    private BrailleDisplayConnection mBrailleDisplayConnection;
+    @Mock
+    private BrailleDisplayConnection.NativeInterface mNativeInterface;
+    @Mock
+    private AccessibilityServiceConnection mServiceConnection;
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    // To mock package-private class
+    @Rule
+    public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+            new DexmakerShareClassLoaderRule();
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mBrailleDisplayConnection = new BrailleDisplayConnection(new Object(), mServiceConnection);
+    }
+
+    @Test
+    public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
+        int descriptorSize = 4;
+        byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
+        when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
+        when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
+    }
+
+    @Test
+    public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
+        when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
+    }
+
+    @Test
+    public void defaultNativeScanner_getUniqueId_returnsUniq() {
+        String macAddress = "12:34:56:78";
+        when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
+    }
+
+    @Test
+    public void defaultNativeScanner_getDeviceBusType_busUsb() {
+        when(mNativeInterface.getHidrawBusType(anyInt()))
+                .thenReturn(BrailleDisplayConnection.BUS_USB);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceBusType(NULL_PATH))
+                .isEqualTo(BrailleDisplayConnection.BUS_USB);
+    }
+
+    @Test
+    public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
+        when(mNativeInterface.getHidrawBusType(anyInt()))
+                .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceBusType(NULL_PATH))
+                .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
+    }
+
+    // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
+    // test Braille display data, but its own implementation should also be tested
+    // so that issues in this helper don't cause confusing failures in CTS.
+    @Test
+    public void setTestData_scannerReturnsTestData() {
+        Bundle bd1 = new Bundle(), bd2 = new Bundle();
+
+        Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
+        bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString());
+        bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString());
+        byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
+        bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
+        bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
+        String uniq1 = "uniq1", uniq2 = "uniq2";
+        bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
+        bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
+        int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH;
+        bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+                bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
+        bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+                bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
+
+        expect.that(scanner.getHidrawNodePaths()).containsExactly(path1, path2);
+        expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
+        expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
+        expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
+        expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
+        expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
+        expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
+    }
+}
diff --git a/services/usb/Android.bp b/services/usb/Android.bp
index 3a0a6ab..e8ffe54 100644
--- a/services/usb/Android.bp
+++ b/services/usb/Android.bp
@@ -34,8 +34,20 @@
         "android.hardware.usb-V1.2-java",
         "android.hardware.usb-V1.3-java",
         "android.hardware.usb-V3-java",
+        "usb_flags_lib",
     ],
     lint: {
         baseline_filename: "lint-baseline.xml",
     },
 }
+
+aconfig_declarations {
+    name: "usb_flags",
+    package: "com.android.server.usb.flags",
+    srcs: ["**/usb_flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "usb_flags_lib",
+    aconfig_declarations: "usb_flags",
+}
diff --git a/services/usb/java/com/android/server/usb/UsbHandlerManager.java b/services/usb/java/com/android/server/usb/UsbHandlerManager.java
index f311274..d83ff1f 100644
--- a/services/usb/java/com/android/server/usb/UsbHandlerManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHandlerManager.java
@@ -37,7 +37,7 @@
  *
  * @hide
  */
-class UsbHandlerManager {
+public class UsbHandlerManager {
     private static final String LOG_TAG = UsbHandlerManager.class.getSimpleName();
 
     private final Context mContext;
diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
index f916660..2ff21ad 100644
--- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -62,6 +63,7 @@
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.usb.flags.Flags;
 import com.android.server.utils.EventLogger;
 
 import libcore.io.IoUtils;
@@ -80,8 +82,20 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
-class UsbProfileGroupSettingsManager {
+public class UsbProfileGroupSettingsManager {
+    /**
+     * &lt;application&gt; level property that an app can specify to restrict any overlaying of
+     * activities when usb device is attached.
+     *
+     *
+     * <p>This should only be set by privileged apps having {@link Manifest.permission#MANAGE_USB}
+     * permission.
+     * @hide
+     */
+    public static final String PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES =
+            "android.app.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES";
     private static final String TAG = UsbProfileGroupSettingsManager.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -101,6 +115,8 @@
 
     private final PackageManager mPackageManager;
 
+    private final ActivityManager mActivityManager;
+
     private final UserManager mUserManager;
     private final @NonNull UsbSettingsManager mSettingsManager;
 
@@ -224,7 +240,7 @@
      * @param settingsManager The settings manager of the service
      * @param usbResolveActivityManager The resovle activity manager of the service
      */
-    UsbProfileGroupSettingsManager(@NonNull Context context, @NonNull UserHandle user,
+    public UsbProfileGroupSettingsManager(@NonNull Context context, @NonNull UserHandle user,
             @NonNull UsbSettingsManager settingsManager,
             @NonNull UsbHandlerManager usbResolveActivityManager) {
         if (DEBUG) Slog.v(TAG, "Creating settings for " + user);
@@ -238,6 +254,7 @@
 
         mContext = context;
         mPackageManager = context.getPackageManager();
+        mActivityManager = context.getSystemService(ActivityManager.class);
         mSettingsManager = settingsManager;
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
 
@@ -895,7 +912,10 @@
         // Send broadcast to running activities with registered intent
         mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
 
-        resolveActivity(intent, device, true /* showMtpNotification */);
+        //resolving activities only if there is no foreground activity restricting it.
+        if (!shouldRestrictOverlayActivities()) {
+            resolveActivity(intent, device, true /* showMtpNotification */);
+        }
     }
 
     private void resolveActivity(Intent intent, UsbDevice device, boolean showMtpNotification) {
@@ -918,6 +938,63 @@
         resolveActivity(intent, matches, defaultActivity, device, null);
     }
 
+    /**
+     * @return true if any application in foreground have set restrict_usb_overlay_activities as
+     * true in manifest file. The application needs to have MANAGE_USB permission.
+     */
+    private boolean shouldRestrictOverlayActivities() {
+
+        if (!Flags.allowRestrictionOfOverlayActivities()) return false;
+
+        List<ActivityManager.RunningAppProcessInfo> appProcessInfos = mActivityManager
+                .getRunningAppProcesses();
+
+        List<String> filteredAppProcessInfos = new ArrayList<>();
+        boolean shouldRestrictOverlayActivities;
+
+        //filtering out applications in foreground.
+        for (ActivityManager.RunningAppProcessInfo processInfo : appProcessInfos) {
+            if (processInfo.importance <= ActivityManager
+                    .RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+                filteredAppProcessInfos.addAll(List.of(processInfo.pkgList));
+            }
+        }
+
+        if (DEBUG) Slog.d(TAG, "packages in foreground : " + filteredAppProcessInfos);
+
+        List<String> packagesHoldingManageUsbPermission =
+                mPackageManager.getPackagesHoldingPermissions(
+                        new String[]{android.Manifest.permission.MANAGE_USB},
+                        PackageManager.MATCH_SYSTEM_ONLY).stream()
+                        .map(packageInfo -> packageInfo.packageName).collect(Collectors.toList());
+
+        //retaining only packages that hold the required permission
+        filteredAppProcessInfos.retainAll(packagesHoldingManageUsbPermission);
+
+        if (DEBUG) {
+            Slog.d(TAG, "packages in foreground with required permission : "
+                    + filteredAppProcessInfos);
+        }
+
+        shouldRestrictOverlayActivities = filteredAppProcessInfos.stream().anyMatch(pkg -> {
+            try {
+                return mPackageManager.getProperty(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES, pkg)
+                        .getBoolean();
+            } catch (NameNotFoundException e) {
+                if (DEBUG) {
+                    Slog.d(TAG, "property PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES "
+                            + "not present for " + pkg);
+                }
+                return false;
+            }
+        });
+
+        if (shouldRestrictOverlayActivities) {
+            Slog.d(TAG, "restricting starting of usb overlay activities");
+        }
+        return shouldRestrictOverlayActivities;
+    }
+
     public void deviceAttachedForFixedHandler(UsbDevice device, ComponentName component) {
         final Intent intent = createDeviceAttachedIntent(device);
 
diff --git a/services/usb/java/com/android/server/usb/UsbSettingsManager.java b/services/usb/java/com/android/server/usb/UsbSettingsManager.java
index 8e53ff4..0b854a8 100644
--- a/services/usb/java/com/android/server/usb/UsbSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbSettingsManager.java
@@ -33,7 +33,7 @@
 /**
  * Maintains all {@link UsbUserSettingsManager} for all users.
  */
-class UsbSettingsManager {
+public class UsbSettingsManager {
     private static final String LOG_TAG = UsbSettingsManager.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -70,7 +70,7 @@
      *
      * @return The settings for the user
      */
-    @NonNull UsbUserSettingsManager getSettingsForUser(@UserIdInt int userId) {
+    public @NonNull UsbUserSettingsManager getSettingsForUser(@UserIdInt int userId) {
         synchronized (mSettingsByUser) {
             UsbUserSettingsManager settings = mSettingsByUser.get(userId);
             if (settings == null) {
diff --git a/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java b/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
index c2b8d01..be729c5 100644
--- a/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
@@ -49,7 +49,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-class UsbUserSettingsManager {
+public class UsbUserSettingsManager {
     private static final String TAG = UsbUserSettingsManager.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -81,7 +81,7 @@
      *
      * @return The resolve infos of the activities that can handle the intent
      */
-    List<ResolveInfo> queryIntentActivities(@NonNull Intent intent) {
+    public List<ResolveInfo> queryIntentActivities(@NonNull Intent intent) {
         return mPackageManager.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA,
                 mUser.getIdentifier());
     }
diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
new file mode 100644
index 0000000..ea6e502
--- /dev/null
+++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.usb.flags"
+
+flag {
+    name: "allow_restriction_of_overlay_activities"
+    namespace: "usb"
+    description: "This flag controls the restriction of usb overlay activities"
+    bug: "307231174"
+}
diff --git a/telephony/java/android/telephony/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java
index 3c11da5..4ff9712 100644
--- a/telephony/java/android/telephony/DomainSelectionService.java
+++ b/telephony/java/android/telephony/DomainSelectionService.java
@@ -831,7 +831,7 @@
             @NonNull String tag, @NonNull String errorLogName) {
         try {
             CompletableFuture.runAsync(
-                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor).join();
+                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor);
         } catch (CancellationException | CompletionException e) {
             Rlog.w(tag, "Binder - " + errorLogName + " exception: " + e.getMessage());
         }
diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp
index c02d8e9..70c7dad 100644
--- a/tests/UsbManagerTests/Android.bp
+++ b/tests/UsbManagerTests/Android.bp
@@ -29,12 +29,17 @@
     static_libs: [
         "frameworks-base-testutils",
         "androidx.test.rules",
-        "mockito-target-inline-minus-junit4",
+        "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "truth",
         "UsbManagerTestLib",
     ],
-    jni_libs: ["libdexmakerjvmtiagent"],
+    jni_libs: [
+        // Required for ExtendedMockito
+        "libdexmakerjvmtiagent",
+        "libmultiplejvmtiagentsinterferenceagent",
+        "libstaticjvmtiagent",
+    ],
     certificate: "platform",
     platform_apis: true,
     test_suites: ["device-tests"],
diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
new file mode 100644
index 0000000..4780d8a
--- /dev/null
+++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.usbtest;
+
+import static com.android.server.usb.UsbProfileGroupSettingsManager.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.Property;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.hardware.usb.UsbDevice;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.usb.UsbHandlerManager;
+import com.android.server.usb.UsbProfileGroupSettingsManager;
+import com.android.server.usb.UsbSettingsManager;
+import com.android.server.usb.UsbUserSettingsManager;
+import com.android.server.usb.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link com.android.server.usb.UsbProfileGroupSettingsManager}.
+ * Note: MUST claim MANAGE_USB permission in Manifest
+ */
+@RunWith(AndroidJUnit4.class)
+public class UsbProfileGroupSettingsManagerTest {
+
+    private static final String TEST_PACKAGE_NAME = "testPkg";
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private ActivityManager mActivityManager;
+    @Mock
+    private UserHandle mUserHandle;
+    @Mock
+    private UsbSettingsManager mUsbSettingsManager;
+    @Mock
+    private UsbHandlerManager mUsbHandlerManager;
+    @Mock
+    private UserManager mUserManager;
+    @Mock
+    private UsbUserSettingsManager mUsbUserSettingsManager;
+    @Mock private Property mProperty;
+    private ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo;
+    private PackageInfo mPackageInfo;
+    private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager;
+    private MockitoSession mStaticMockSession;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mRunningAppProcessInfo = new ActivityManager.RunningAppProcessInfo();
+        mRunningAppProcessInfo.pkgList = new String[]{TEST_PACKAGE_NAME};
+        mPackageInfo = new PackageInfo();
+        mPackageInfo.packageName = TEST_PACKAGE_NAME;
+        mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
+
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
+        when(mContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+        when(mContext.createPackageContextAsUser(anyString(), anyInt(), any(UserHandle.class)))
+                .thenReturn(mContext);
+        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+
+        mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(mContext, mUserHandle,
+                mUsbSettingsManager, mUsbHandlerManager);
+
+        mStaticMockSession = ExtendedMockito.mockitoSession()
+                .mockStatic(Flags.class)
+                .strictness(Strictness.WARN)
+                .startMocking();
+
+        when(mPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mPackageInfo);
+        when(mPackageManager.getProperty(eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES),
+                eq(TEST_PACKAGE_NAME))).thenReturn(mProperty);
+        when(mUserManager.getEnabledProfiles(anyInt()))
+                .thenReturn(List.of(Mockito.mock(UserInfo.class)));
+        when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mStaticMockSession.finishMocking();
+    }
+
+    @Test
+    public void testDeviceAttached_flagTrueWithoutForegroundActivity_resolveActivityCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(new ArrayList<>());
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_noForegroundActivityWithUsbPermission_resolveActivityCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(new ArrayList<>());
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+        when(mProperty.getBoolean()).thenReturn(true);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager, times(0))
+                .queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_foregroundActivityWithoutManifestField_resolveActivityCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+        when(mProperty.getBoolean()).thenReturn(false);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_flagFalseForegroundActivity_resolveActivityCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(false);
+        when(mProperty.getBoolean()).thenReturn(true);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+    }
+}