Merge "Revert "Synchronize cache-is-ready boolean with cache readiness."" into tm-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index afe36b5..d5a7f28 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -959,7 +959,7 @@
         for (int i = 0; i < mActiveServices.size(); i++) {
             JobServiceContext jsc = mActiveServices.get(i);
             final JobStatus executing = jsc.getRunningJobLocked();
-            if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
+            if (executing == job) {
                 jsc.cancelExecutingJobLocked(reason, internalReasonCode, debugReason);
                 return true;
             }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
index 428f2cb..0f385ef 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -17,6 +17,7 @@
 package com.android.server.job.controllers;
 
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 import static com.android.server.job.JobSchedulerService.sSystemClock;
@@ -90,6 +91,14 @@
     @CurrentTimeMillisLong
     private long mLaunchTimeThresholdMs = PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
 
+    /**
+     * The additional time we'll add to a launch time estimate before considering it obsolete and
+     * try to get a new estimate. This will help make prefetch jobs more viable in case an estimate
+     * is a few minutes early.
+     */
+    @GuardedBy("mLock")
+    private long mLaunchTimeAllowanceMs = PcConstants.DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
+
     @SuppressWarnings("FieldCanBeLocal")
     private final EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener =
             new EstimatedLaunchTimeChangedListener() {
@@ -204,7 +213,8 @@
     private long getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName,
             @CurrentTimeMillisLong long now) {
         final Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
-        if (nextEstimatedLaunchTime == null || nextEstimatedLaunchTime < now) {
+        if (nextEstimatedLaunchTime == null
+                || nextEstimatedLaunchTime < now - mLaunchTimeAllowanceMs) {
             // Don't query usage stats here because it may have to read from disk.
             mHandler.obtainMessage(MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME, userId, 0, pkgName)
                     .sendToTarget();
@@ -335,7 +345,9 @@
         }
 
         final long nextEstimatedLaunchTime = getNextEstimatedLaunchTimeLocked(userId, pkgName, now);
-        if (nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) {
+        // Avoid setting an alarm for the end of time.
+        if (nextEstimatedLaunchTime != Long.MAX_VALUE
+                && nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) {
             // Set alarm to be notified when this crosses the threshold.
             final long timeToCrossThresholdMs =
                     nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs);
@@ -354,7 +366,7 @@
     private boolean willBeLaunchedSoonLocked(int userId, @NonNull String pkgName,
             @CurrentTimeMillisLong long now) {
         return getNextEstimatedLaunchTimeLocked(userId, pkgName, now)
-                <= now + mLaunchTimeThresholdMs;
+                <= now + mLaunchTimeThresholdMs - mLaunchTimeAllowanceMs;
     }
 
     @Override
@@ -494,16 +506,37 @@
         @VisibleForTesting
         static final String KEY_LAUNCH_TIME_THRESHOLD_MS =
                 PC_CONSTANT_PREFIX + "launch_time_threshold_ms";
+        @VisibleForTesting
+        static final String KEY_LAUNCH_TIME_ALLOWANCE_MS =
+                PC_CONSTANT_PREFIX + "launch_time_allowance_ms";
 
         private static final long DEFAULT_LAUNCH_TIME_THRESHOLD_MS = 7 * HOUR_IN_MILLIS;
+        private static final long DEFAULT_LAUNCH_TIME_ALLOWANCE_MS = 20 * MINUTE_IN_MILLIS;
 
         /** How much time each app will have to run jobs within their standby bucket window. */
         public long LAUNCH_TIME_THRESHOLD_MS = DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
 
+        /**
+         * How much additional time to add to an estimated launch time before considering it
+         * unusable.
+         */
+        public long LAUNCH_TIME_ALLOWANCE_MS = DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
+
         @GuardedBy("mLock")
         public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
                 @NonNull String key) {
             switch (key) {
+                case KEY_LAUNCH_TIME_ALLOWANCE_MS:
+                    LAUNCH_TIME_ALLOWANCE_MS =
+                            properties.getLong(key, DEFAULT_LAUNCH_TIME_ALLOWANCE_MS);
+                    // Limit the allowance to the range [0 minutes, 2 hours].
+                    long newLaunchTimeAllowanceMs = Math.min(2 * HOUR_IN_MILLIS,
+                            Math.max(0, LAUNCH_TIME_ALLOWANCE_MS));
+                    if (mLaunchTimeAllowanceMs != newLaunchTimeAllowanceMs) {
+                        mLaunchTimeAllowanceMs = newLaunchTimeAllowanceMs;
+                        mShouldReevaluateConstraints = true;
+                    }
+                    break;
                 case KEY_LAUNCH_TIME_THRESHOLD_MS:
                     LAUNCH_TIME_THRESHOLD_MS =
                             properties.getLong(key, DEFAULT_LAUNCH_TIME_THRESHOLD_MS);
@@ -528,6 +561,7 @@
             pw.increaseIndent();
 
             pw.print(KEY_LAUNCH_TIME_THRESHOLD_MS, LAUNCH_TIME_THRESHOLD_MS).println();
+            pw.print(KEY_LAUNCH_TIME_ALLOWANCE_MS, LAUNCH_TIME_ALLOWANCE_MS).println();
 
             pw.decreaseIndent();
         }
@@ -536,6 +570,11 @@
     //////////////////////// TESTING HELPERS /////////////////////////////
 
     @VisibleForTesting
+    long getLaunchTimeAllowanceMs() {
+        return mLaunchTimeAllowanceMs;
+    }
+
+    @VisibleForTesting
     long getLaunchTimeThresholdMs() {
         return mLaunchTimeThresholdMs;
     }
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index a216021..cb64173 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2709,7 +2709,7 @@
             AppOpsManager.MODE_ALLOWED, // READ_ICC_SMS
             AppOpsManager.MODE_ALLOWED, // WRITE_ICC_SMS
             AppOpsManager.MODE_DEFAULT, // WRITE_SETTINGS
-            AppOpsManager.MODE_DEFAULT, // SYSTEM_ALERT_WINDOW /*Overridden in opToDefaultMode()*/
+            getSystemAlertWindowDefault(), // SYSTEM_ALERT_WINDOW
             AppOpsManager.MODE_ALLOWED, // ACCESS_NOTIFICATIONS
             AppOpsManager.MODE_ALLOWED, // CAMERA
             AppOpsManager.MODE_ALLOWED, // RECORD_AUDIO
@@ -3163,8 +3163,6 @@
     private static final String DEBUG_LOGGING_OPS_PROP = "appops.logging_ops";
     private static final String DEBUG_LOGGING_TAG = "AppOpsManager";
 
-    private static volatile Integer sOpSystemAlertWindowDefaultMode;
-
     /**
      * Retrieve the op switch that controls the given operation.
      * @hide
@@ -3263,9 +3261,6 @@
      * @hide
      */
     public static @Mode int opToDefaultMode(int op) {
-        if (op == OP_SYSTEM_ALERT_WINDOW) {
-            return getSystemAlertWindowDefault();
-        }
         return sOpDefaultMode[op];
     }
 
@@ -10283,11 +10278,6 @@
     }
 
     private static int getSystemAlertWindowDefault() {
-        // This is indeed racy but we aren't expecting the result to change so it's not worth
-        // the synchronization.
-        if (sOpSystemAlertWindowDefaultMode != null) {
-            return sOpSystemAlertWindowDefaultMode;
-        }
         final Context context = ActivityThread.currentApplication();
         if (context == null) {
             return AppOpsManager.MODE_DEFAULT;
@@ -10298,11 +10288,10 @@
         // TVs are constantly plugged in and has less concern for memory/power
         if (ActivityManager.isLowRamDeviceStatic()
                 && !pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK, 0)) {
-            sOpSystemAlertWindowDefaultMode = AppOpsManager.MODE_IGNORED;
-        } else {
-            sOpSystemAlertWindowDefaultMode = AppOpsManager.MODE_DEFAULT;
+            return AppOpsManager.MODE_IGNORED;
         }
-        return sOpSystemAlertWindowDefaultMode;
+
+        return AppOpsManager.MODE_DEFAULT;
     }
 
     /**
diff --git a/core/java/android/content/res/AssetFileDescriptor.java b/core/java/android/content/res/AssetFileDescriptor.java
index dd00c3a..ac65933 100644
--- a/core/java/android/content/res/AssetFileDescriptor.java
+++ b/core/java/android/content/res/AssetFileDescriptor.java
@@ -21,8 +21,6 @@
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
-import android.system.ErrnoException;
-import android.system.Os;
 
 import java.io.Closeable;
 import java.io.FileDescriptor;
@@ -54,11 +52,11 @@
     /**
      * Create a new AssetFileDescriptor from the given values.
      *
-     * @param fd          The underlying file descriptor.
+     * @param fd The underlying file descriptor.
      * @param startOffset The location within the file that the asset starts.
-     *                    This must be 0 if length is UNKNOWN_LENGTH.
-     * @param length      The number of bytes of the asset, or
-     *                    {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
+     *            This must be 0 if length is UNKNOWN_LENGTH.
+     * @param length The number of bytes of the asset, or
+     *            {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
      */
     public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset,
             long length) {
@@ -68,13 +66,13 @@
     /**
      * Create a new AssetFileDescriptor from the given values.
      *
-     * @param fd          The underlying file descriptor.
+     * @param fd The underlying file descriptor.
      * @param startOffset The location within the file that the asset starts.
-     *                    This must be 0 if length is UNKNOWN_LENGTH.
-     * @param length      The number of bytes of the asset, or
-     *                    {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
-     * @param extras      additional details that can be used to interpret the
-     *                    underlying file descriptor. May be null.
+     *            This must be 0 if length is UNKNOWN_LENGTH.
+     * @param length The number of bytes of the asset, or
+     *            {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
+     * @param extras additional details that can be used to interpret the
+     *            underlying file descriptor. May be null.
      */
     public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset,
             long length, Bundle extras) {
@@ -205,24 +203,19 @@
      */
     public static class AutoCloseInputStream
             extends ParcelFileDescriptor.AutoCloseInputStream {
-        private final long mSizeFromStartOffset;
-        private final long mStartOffset;
-        private long mPosFromStartOffset;
+        private long mRemaining;
 
         public AutoCloseInputStream(AssetFileDescriptor fd) throws IOException {
             super(fd.getParcelFileDescriptor());
-            // this skip is necessary if getChannel() is called
             super.skip(fd.getStartOffset());
-            mSizeFromStartOffset = fd.getLength();
-            mStartOffset = fd.getStartOffset();
+            mRemaining = (int) fd.getLength();
         }
 
         @Override
         public int available() throws IOException {
-            long available = mSizeFromStartOffset - mPosFromStartOffset;
-            return available >= 0
-                    ? (available < 0x7fffffff ? (int) available : 0x7fffffff)
-                    : 0;
+            return mRemaining >= 0
+                    ? (mRemaining < 0x7fffffff ? (int) mRemaining : 0x7fffffff)
+                    : super.available();
         }
 
         @Override
@@ -234,24 +227,15 @@
 
         @Override
         public int read(byte[] buffer, int offset, int count) throws IOException {
-            int available = available();
-
-            if (available <= 0) {
-                return -1;
-            } else {
-                if (count > available) count = available;
-                try {
-                    int res = Os.pread(getFD(), buffer, offset, count,
-                            mStartOffset + mPosFromStartOffset);
-                    // pread returns 0 at end of file, while java's InputStream interface
-                    // requires -1
-                    if (res == 0) res = -1;
-                    if (res >= 0) mPosFromStartOffset += res;
-                    return res;
-                } catch (ErrnoException e) {
-                    throw new IOException(e);
-                }
+            if (mRemaining >= 0) {
+                if (mRemaining == 0) return -1;
+                if (count > mRemaining) count = (int) mRemaining;
+                int res = super.read(buffer, offset, count);
+                if (res >= 0) mRemaining -= res;
+                return res;
             }
+
+            return super.read(buffer, offset, count);
         }
 
         @Override
@@ -261,31 +245,41 @@
 
         @Override
         public long skip(long count) throws IOException {
-            int available = available();
-            if (available <= 0) {
-                return -1;
-            } else {
-                if (count > available) count = available;
-                mPosFromStartOffset += count;
-                return count;
+            if (mRemaining >= 0) {
+                if (mRemaining == 0) return -1;
+                if (count > mRemaining) count = mRemaining;
+                long res = super.skip(count);
+                if (res >= 0) mRemaining -= res;
+                return res;
             }
+
+            return super.skip(count);
         }
 
         @Override
         public void mark(int readlimit) {
-            // Not supported.
-            return;
+            if (mRemaining >= 0) {
+                // Not supported.
+                return;
+            }
+            super.mark(readlimit);
         }
 
         @Override
         public boolean markSupported() {
-            return false;
+            if (mRemaining >= 0) {
+                return false;
+            }
+            return super.markSupported();
         }
 
         @Override
         public synchronized void reset() throws IOException {
-            // Not supported.
-            return;
+            if (mRemaining >= 0) {
+                // Not supported.
+                return;
+            }
+            super.reset();
         }
     }
 
@@ -381,7 +375,6 @@
         public AssetFileDescriptor createFromParcel(Parcel in) {
             return new AssetFileDescriptor(in);
         }
-
         public AssetFileDescriptor[] newArray(int size) {
             return new AssetFileDescriptor[size];
         }
diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java
index 0d3aaf5..10c3730 100644
--- a/core/java/android/hardware/Sensor.java
+++ b/core/java/android/hardware/Sensor.java
@@ -712,14 +712,16 @@
     public static final String STRING_TYPE_HINGE_ANGLE = "android.sensor.hinge_angle";
 
     /**
-     * A constant describing a head tracker sensor.
+     * A constant describing a head tracker sensor. Note that this sensor type is typically not
+     * available for apps to use.
      *
      * See {@link android.hardware.SensorEvent#values SensorEvent.values} for more details.
      */
     public static final int TYPE_HEAD_TRACKER = 37;
 
     /**
-     * A constant string describing a head tracker sensor.
+     * A constant string describing a head tracker sensor. Note that this sensor type is typically
+     * not available for apps to use.
      *
      * See {@link android.hardware.SensorEvent#values SensorEvent.values} for more details.
      */
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 200fe22..21ecf8b 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -592,7 +592,7 @@
 
     private InlineSuggestionSessionController mInlineSuggestionSessionController;
 
-    private boolean mAutomotiveHideNavBarForKeyboard;
+    private boolean mHideNavBarForKeyboard;
     private boolean mIsAutomotive;
     private @NonNull OptionalInt mHandwritingRequestId = OptionalInt.empty();
     private InputEventReceiver mHandwritingEventReceiver;
@@ -1498,9 +1498,8 @@
         // shown the first time (cold start).
         mSettingsObserver.shouldShowImeWithHardKeyboard();
 
-        mIsAutomotive = isAutomotive();
-        mAutomotiveHideNavBarForKeyboard = getApplicationContext().getResources().getBoolean(
-                com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard);
+        mHideNavBarForKeyboard = getApplicationContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_hideNavBarForKeyboard);
 
         // TODO(b/111364446) Need to address context lifecycle issue if need to re-create
         // for update resources & configuration correctly when show soft input
@@ -1539,11 +1538,11 @@
             window.setFlags(windowFlags, windowFlagsMask);
 
             // Automotive devices may request the navigation bar to be hidden when the IME shows up
-            // (controlled via config_automotiveHideNavBarForKeyboard) in order to maximize the
-            // visible screen real estate. When this happens, the IME window should animate from the
+            // (controlled via config_hideNavBarForKeyboard) in order to maximize the visible
+            // screen real estate. When this happens, the IME window should animate from the
             // bottom of the screen to reduce the jank that happens from the lack of synchronization
             // between the bottom system window and the IME window.
-            if (mIsAutomotive && mAutomotiveHideNavBarForKeyboard) {
+            if (mHideNavBarForKeyboard) {
                 window.setDecorFitsSystemWindows(false);
             }
         }
diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java
index 6a65efb..6b59f54 100644
--- a/core/java/android/speech/RecognitionService.java
+++ b/core/java/android/speech/RecognitionService.java
@@ -129,8 +129,9 @@
             @NonNull AttributionSource attributionSource) {
         try {
             if (mCurrentCallback == null) {
-                boolean preflightPermissionCheckPassed = checkPermissionForPreflightNotHardDenied(
-                        attributionSource);
+                boolean preflightPermissionCheckPassed =
+                        intent.hasExtra(RecognizerIntent.EXTRA_AUDIO_SOURCE)
+                        || checkPermissionForPreflightNotHardDenied(attributionSource);
                 if (preflightPermissionCheckPassed) {
                     if (DBG) {
                         Log.d(TAG, "created new mCurrentCallback, listener = "
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 2ad1b38..7cc37f7 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -190,6 +190,7 @@
     private static final String SHORTCUT_TARGET = "shortcut_target";
     private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20;
     public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter";
+    private static final String SHARED_TEXT_KEY = "shared_text";
 
     private static final String PLURALS_COUNT = "count";
     private static final String PLURALS_FILE_NAME = "file_name";
@@ -2201,6 +2202,7 @@
         final IntentFilter filter = getTargetIntentFilter();
         Bundle extras = new Bundle();
         extras.putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, filter);
+        populateTextContent(extras);
         AppPredictionContext appPredictionContext = new AppPredictionContext.Builder(contextAsUser)
             .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
             .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
@@ -2219,6 +2221,12 @@
         return appPredictionSession;
     }
 
+    private void populateTextContent(Bundle extras) {
+        final Intent intent = getTargetIntent();
+        String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
+        extras.putString(SHARED_TEXT_KEY, sharedText);
+    }
+
     /**
      * This will return an app predictor if it is enabled for direct share sorting
      * and if one exists. Otherwise, it returns null.
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 2e4860a..2dcc585 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -74,6 +74,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__UNFOLD_ANIM;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__VOLUME_CONTROL;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION;
 
 import android.annotation.IntDef;
@@ -196,6 +197,7 @@
     public static final int CUJ_SPLIT_SCREEN_RESIZE = 52;
     public static final int CUJ_SETTINGS_SLIDER = 53;
     public static final int CUJ_TAKE_SCREENSHOT = 54;
+    public static final int CUJ_VOLUME_CONTROL = 55;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -259,6 +261,7 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLIT_SCREEN_RESIZE,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_SLIDER,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__VOLUME_CONTROL,
     };
 
     private static volatile InteractionJankMonitor sInstance;
@@ -334,6 +337,7 @@
             CUJ_SPLIT_SCREEN_RESIZE,
             CUJ_SETTINGS_SLIDER,
             CUJ_TAKE_SCREENSHOT,
+            CUJ_VOLUME_CONTROL,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -762,6 +766,8 @@
                 return "SETTINGS_SLIDER";
             case CUJ_TAKE_SCREENSHOT:
                 return "TAKE_SCREENSHOT";
+            case CUJ_VOLUME_CONTROL:
+                return "VOLUME_CONTROL";
         }
         return "UNKNOWN";
     }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f20b824..8db5f9a 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7044,7 +7044,7 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
-        <service android:name="com.android.server.companion.AssociationCleanUpService"
+        <service android:name="com.android.server.companion.InactiveAssociationsRemovalService"
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2b0d01f..b139e47 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4894,7 +4894,7 @@
     <!-- Whether or not to hide the navigation bar when the soft keyboard is visible in order to
          create additional screen real estate outside beyond the keyboard. Note that the user needs
          to have a confirmed way to dismiss the keyboard when desired. -->
-    <bool name="config_automotiveHideNavBarForKeyboard">false</bool>
+    <bool name="config_hideNavBarForKeyboard">false</bool>
 
     <!-- Whether or not to show the built-in charging animation when the device begins charging
          wirelessly. -->
@@ -5754,6 +5754,14 @@
     -->
     <integer name="config_bg_current_drain_location_min_duration">1800</integer>
 
+    <!-- The behavior when the system detects it has abusive current drains, whether or not to
+         move the app to the restricted standby bucket level.
+         True - we'll move the app to restricted standby bucket as long as its bg battery usage
+         goes beyond the threshold, False - we'll not move it.
+         Note: This should be only enabled on devices with high confidence on power measurement.
+    -->
+    <bool name="config_bg_current_drain_auto_restrict_abusive_apps">false</bool>
+
     <!-- The behavior for an app with a FGS and its notification is still showing, when the system
          detects it's abusive and should be put into bg restricted level. True - we'll
          show the prompt to user, False - we'll not show it.
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b385ac9..77007afc 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4154,7 +4154,7 @@
   <java-symbol type="bool" name="config_disable_all_cb_messages" />
   <java-symbol type="drawable" name="ic_close" />
 
-  <java-symbol type="bool" name="config_automotiveHideNavBarForKeyboard" />
+  <java-symbol type="bool" name="config_hideNavBarForKeyboard" />
 
   <java-symbol type="bool" name="config_showBuiltinWirelessChargingAnim" />
 
@@ -4774,6 +4774,7 @@
   <java-symbol type="array" name="config_bg_current_drain_high_threshold_to_bg_restricted" />
   <java-symbol type="integer" name="config_bg_current_drain_media_playback_min_duration" />
   <java-symbol type="integer" name="config_bg_current_drain_location_min_duration" />
+  <java-symbol type="bool" name="config_bg_current_drain_auto_restrict_abusive_apps" />
   <java-symbol type="bool" name="config_bg_prompt_fgs_with_noti_to_bg_restricted" />
   <java-symbol type="bool" name="config_bg_prompt_abusive_apps_to_bg_restricted" />
   <java-symbol type="integer" name="config_bg_current_drain_exempted_types" />
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index f407bdc..a459319 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -96,6 +96,8 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
+import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -158,6 +160,8 @@
     private final ShellExecutor mMainExecutor;
     private final Handler mMainHandler;
 
+    private final ShellExecutor mBackgroundExecutor;
+
     private BubbleLogger mLogger;
     private BubbleData mBubbleData;
     @Nullable private BubbleStackView mStackView;
@@ -234,8 +238,9 @@
             DisplayController displayController,
             Optional<OneHandedController> oneHandedOptional,
             DragAndDropController dragAndDropController,
-            ShellExecutor mainExecutor,
-            Handler mainHandler,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
         BubbleLogger logger = new BubbleLogger(uiEventLogger);
@@ -245,7 +250,7 @@
                 new BubbleDataRepository(context, launcherApps, mainExecutor),
                 statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
                 logger, taskStackListener, organizer, positioner, displayController,
-                oneHandedOptional, dragAndDropController, mainExecutor, mainHandler,
+                oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
                 taskViewTransitions, syncQueue);
     }
 
@@ -269,8 +274,9 @@
             DisplayController displayController,
             Optional<OneHandedController> oneHandedOptional,
             DragAndDropController dragAndDropController,
-            ShellExecutor mainExecutor,
-            Handler mainHandler,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
         mContext = context;
@@ -286,6 +292,7 @@
         mLogger = bubbleLogger;
         mMainExecutor = mainExecutor;
         mMainHandler = mainHandler;
+        mBackgroundExecutor = bgExecutor;
         mTaskStackListener = taskStackListener;
         mTaskOrganizer = organizer;
         mSurfaceSynchronizer = synchronizer;
@@ -725,7 +732,8 @@
 
         try {
             mAddedToWindowManager = false;
-            mContext.unregisterReceiver(mBroadcastReceiver);
+            // Put on background for this binder call, was causing jank
+            mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver));
             if (mStackView != null) {
                 mWindowManager.removeView(mStackView);
                 mBubbleData.getOverflow().cleanUpExpandedState();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 3b83f15..6a2acf4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -498,6 +498,11 @@
                 dispatchVisibilityChanged(mDisplayId, isShowing);
             }
         }
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        public InsetsSourceControl getImeSourceControl() {
+            return mImeSourceControl;
+        }
     }
 
     void removeImeSurface() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 1bc9e31..98de60c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ChoreographerSfVsync;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformTaskListener;
@@ -114,13 +115,15 @@
             DragAndDropController dragAndDropController,
             @ShellMainThread ShellExecutor mainExecutor,
             @ShellMainThread Handler mainHandler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
         return BubbleController.create(context, null /* synchronizer */,
                 floatingContentCoordinator, statusBarService, windowManager,
                 windowManagerShellWrapper, launcherApps, taskStackListener,
                 uiEventLogger, organizer, displayController, oneHandedOptional,
-                dragAndDropController, mainExecutor, mainHandler, taskViewTransitions, syncQueue);
+                dragAndDropController, mainExecutor, mainHandler, bgExecutor,
+                taskViewTransitions, syncQueue);
     }
 
     //
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 86ae399..5a67eb9 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -134,7 +134,7 @@
     skpCaptureEnabled = debuggingEnabled && base::GetBoolProperty(PROPERTY_CAPTURE_SKP_ENABLED, false);
 
     SkAndroidFrameworkTraceUtil::setEnableTracing(
-            base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, true));
+            base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, false));
 
     runningInEmulator = base::GetBoolProperty(PROPERTY_IS_EMULATOR, false);
 
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl
index fed86dc..2e792b3 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl
@@ -22,10 +22,10 @@
  * Interface to receive callbacks from ITvInteractiveAppManager regardless of sessions.
  * @hide
  */
-interface ITvInteractiveAppManagerCallback {
+oneway interface ITvInteractiveAppManagerCallback {
     void onInteractiveAppServiceAdded(in String iAppServiceId);
     void onInteractiveAppServiceRemoved(in String iAppServiceId);
     void onInteractiveAppServiceUpdated(in String iAppServiceId);
     void onTvInteractiveAppServiceInfoUpdated(in TvInteractiveAppServiceInfo tvIAppInfo);
     void onStateChanged(in String iAppServiceId, int type, int state, int err);
-}
\ No newline at end of file
+}
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index c8d2d1e..5850a81 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -1294,45 +1294,46 @@
     std::string defaultMsg = "Unknown Error";
 
     /* translate OS errors to Java API CryptoException errorCodes (which are positive) */
+    jint jerr = 0;
     switch (err) {
         case ERROR_DRM_NO_LICENSE:
-            err = gCryptoErrorCodes.cryptoErrorNoKey;
+            jerr = gCryptoErrorCodes.cryptoErrorNoKey;
             defaultMsg = "Crypto key not available";
             break;
         case ERROR_DRM_LICENSE_EXPIRED:
-            err = gCryptoErrorCodes.cryptoErrorKeyExpired;
+            jerr = gCryptoErrorCodes.cryptoErrorKeyExpired;
             defaultMsg = "License expired";
             break;
         case ERROR_DRM_RESOURCE_BUSY:
-            err = gCryptoErrorCodes.cryptoErrorResourceBusy;
+            jerr = gCryptoErrorCodes.cryptoErrorResourceBusy;
             defaultMsg = "Resource busy or unavailable";
             break;
         case ERROR_DRM_INSUFFICIENT_OUTPUT_PROTECTION:
-            err = gCryptoErrorCodes.cryptoErrorInsufficientOutputProtection;
+            jerr = gCryptoErrorCodes.cryptoErrorInsufficientOutputProtection;
             defaultMsg = "Required output protections are not active";
             break;
         case ERROR_DRM_SESSION_NOT_OPENED:
-            err = gCryptoErrorCodes.cryptoErrorSessionNotOpened;
+            jerr = gCryptoErrorCodes.cryptoErrorSessionNotOpened;
             defaultMsg = "Attempted to use a closed session";
             break;
         case ERROR_DRM_INSUFFICIENT_SECURITY:
-            err = gCryptoErrorCodes.cryptoErrorInsufficientSecurity;
+            jerr = gCryptoErrorCodes.cryptoErrorInsufficientSecurity;
             defaultMsg = "Required security level is not met";
             break;
         case ERROR_DRM_CANNOT_HANDLE:
-            err = gCryptoErrorCodes.cryptoErrorUnsupportedOperation;
+            jerr = gCryptoErrorCodes.cryptoErrorUnsupportedOperation;
             defaultMsg = "Operation not supported in this configuration";
             break;
         case ERROR_DRM_FRAME_TOO_LARGE:
-            err = gCryptoErrorCodes.cryptoErrorFrameTooLarge;
+            jerr = gCryptoErrorCodes.cryptoErrorFrameTooLarge;
             defaultMsg = "Decrytped frame exceeds size of output buffer";
             break;
         case ERROR_DRM_SESSION_LOST_STATE:
-            err = gCryptoErrorCodes.cryptoErrorLostState;
+            jerr = gCryptoErrorCodes.cryptoErrorLostState;
             defaultMsg = "Session state was lost, open a new session and retry";
             break;
         default:  /* Other negative DRM error codes go out best-effort. */
-            err = MediaErrorToJavaError(err);
+            jerr = MediaErrorToJavaError(err);
             defaultMsg = StrCryptoError(err);
             break;
     }
@@ -1344,7 +1345,7 @@
     jstring msgObj = env->NewStringUTF(msgStr.c_str());
 
     jthrowable exception =
-        (jthrowable)env->NewObject(clazz.get(), constructID, err, msgObj);
+        (jthrowable)env->NewObject(clazz.get(), constructID, jerr, msgObj);
 
     env->Throw(exception);
 }
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 3d6bf15..a389bfc 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -20,7 +20,7 @@
     <string name="app_label">Companion Device Manager</string>
 
     <!-- Title of the device association confirmation dialog. -->
-    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</string>
+    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to access your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</string>
 
     <!-- ================= DEVICE_PROFILE_WATCH and null profile ================= -->
 
@@ -31,10 +31,7 @@
     <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</string>
 
     <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
-    <string name="summary_watch" product="default"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
-
-    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
-    <string name="summary_watch" product="tablet"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
+    <string name="summary_watch">This app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string>
 
     <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
 
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index 9e9ec04..a3aa010 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -497,17 +497,19 @@
         }
 
         final String deviceName = mSelectedDevice.getDisplayName();
-        final Spanned title = getHtmlFromResources(
-                this, R.string.confirmation_title, appLabel, deviceName);
+        final String profileName = getString(R.string.profile_name_watch);
+        final Spanned title;
         final Spanned summary;
         final Drawable profileIcon;
 
         if (deviceProfile == null) {
+            title = getHtmlFromResources(this, R.string.confirmation_title, appLabel, deviceName);
             summary = getHtmlFromResources(this, R.string.summary_generic);
             profileIcon = getIcon(this, R.drawable.ic_device_other);
             mSummary.setVisibility(View.GONE);
         } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) {
-            summary = getHtmlFromResources(this, R.string.summary_watch, appLabel, deviceName);
+            title = getHtmlFromResources(this, R.string.confirmation_title, appLabel, profileName);
+            summary = getHtmlFromResources(this, R.string.summary_watch, deviceName, appLabel);
             profileIcon = getIcon(this, R.drawable.ic_watch);
         } else {
             throw new RuntimeException("Unsupported profile " + deviceProfile);
@@ -535,7 +537,7 @@
             mSummary.setVisibility(View.GONE);
         } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) {
             profileName = getString(R.string.profile_name_watch);
-            summary = getHtmlFromResources(this, R.string.summary_watch, appLabel);
+            summary = getHtmlFromResources(this, R.string.summary_watch, profileName, appLabel);
             profileIcon = getIcon(this, R.drawable.ic_watch);
         } else {
             throw new RuntimeException("Unsupported profile " + deviceProfile);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 6919cf2..4229b68 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -77,6 +77,8 @@
     private final LocalBluetoothProfileManager mProfileManager;
     private final Object mProfileLock = new Object();
     BluetoothDevice mDevice;
+    private int mDeviceSide;
+    private int mDeviceMode;
     private long mHiSyncId;
     private int mGroupId;
     // Need this since there is no method for getting RSSI
@@ -335,6 +337,22 @@
         connectDevice();
     }
 
+    public int getDeviceSide() {
+        return mDeviceSide;
+    }
+
+    public void setDeviceSide(int side) {
+        mDeviceSide = side;
+    }
+
+    public int getDeviceMode() {
+        return mDeviceMode;
+    }
+
+    public void setDeviceMode(int mode) {
+        mDeviceMode = mode;
+    }
+
     public long getHiSyncId() {
         return mHiSyncId;
     }
@@ -1111,7 +1129,8 @@
                 stringRes = R.string.bluetooth_battery_level;
             }
 
-            // Set active string in following device connected situation.
+            // Set active string in following device connected situation, also show battery
+            // information if they have.
             //    1. Hearing Aid device active.
             //    2. Headset device active with in-calling state.
             //    3. A2DP device active without in-calling state.
@@ -1130,6 +1149,24 @@
                         stringRes = R.string.bluetooth_active_no_battery_level;
                     }
                 }
+
+                // Try to show left/right information if can not get it from battery for hearing
+                // aids specifically.
+                if (mIsActiveDeviceHearingAid
+                        && stringRes == R.string.bluetooth_active_no_battery_level) {
+                    final CachedBluetoothDevice subDevice = getSubDevice();
+                    if (subDevice != null && subDevice.isConnected()) {
+                        stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
+                    } else {
+                        if (mDeviceSide == HearingAidProfile.DeviceSide.SIDE_LEFT) {
+                            stringRes = R.string.bluetooth_hearing_aid_left_active;
+                        } else if (mDeviceSide == HearingAidProfile.DeviceSide.SIDE_RIGHT) {
+                            stringRes = R.string.bluetooth_hearing_aid_right_active;
+                        } else {
+                            stringRes = R.string.bluetooth_active_no_battery_level;
+                        }
+                    }
+                }
             }
         }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
index 6f2d4de..a491455 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
@@ -29,13 +29,48 @@
 import android.content.Context;
 import android.util.Log;
 
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
 import com.android.settingslib.R;
 import com.android.settingslib.Utils;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
 
 public class HearingAidProfile implements LocalBluetoothProfile {
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            DeviceSide.SIDE_INVALID,
+            DeviceSide.SIDE_LEFT,
+            DeviceSide.SIDE_RIGHT
+    })
+
+    /** Side definition for hearing aids. See {@link BluetoothHearingAid}. */
+    public @interface DeviceSide {
+        int SIDE_INVALID = -1;
+        int SIDE_LEFT = 0;
+        int SIDE_RIGHT = 1;
+    }
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            DeviceMode.MODE_INVALID,
+            DeviceMode.MODE_MONAURAL,
+            DeviceMode.MODE_BINAURAL
+    })
+
+    /** Mode definition for hearing aids. See {@link BluetoothHearingAid}. */
+    public @interface DeviceMode {
+        int MODE_INVALID = -1;
+        int MODE_MONAURAL = 0;
+        int MODE_BINAURAL = 1;
+    }
+
     private static final String TAG = "HearingAidProfile";
     private static boolean V = true;
 
@@ -212,6 +247,11 @@
         return isEnabled;
     }
 
+    /**
+     * Tells remote device to set an absolute volume.
+     *
+     * @param volume Absolute volume to be set on remote
+     */
     public void setVolume(int volume) {
         if (mService == null) {
             return;
@@ -219,6 +259,12 @@
         mService.setVolume(volume);
     }
 
+    /**
+     * Gets the HiSyncId (unique hearing aid device identifier) of the device.
+     *
+     * @param device Bluetooth device
+     * @return the HiSyncId of the device
+     */
     public long getHiSyncId(BluetoothDevice device) {
         if (mService == null || device == null) {
             return BluetoothHearingAid.HI_SYNC_ID_INVALID;
@@ -226,6 +272,59 @@
         return mService.getHiSyncId(device);
     }
 
+    /**
+     * Gets the side of the device.
+     *
+     * @param device Bluetooth device.
+     * @return side of the device. See {@link DeviceSide}.
+     */
+    @DeviceSide
+    public int getDeviceSide(@NonNull BluetoothDevice device) {
+        final int defaultValue = DeviceSide.SIDE_INVALID;
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to HearingAidService");
+            return defaultValue;
+        }
+
+        try {
+            Method method = mService.getClass().getDeclaredMethod("getDeviceSideInternal",
+                    BluetoothDevice.class);
+            method.setAccessible(true);
+            return (int) method.invoke(mService, device);
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+            Log.e(TAG, "fail to get getDeviceSideInternal\n" + e.toString() + "\n"
+                    + Log.getStackTraceString(new Throwable()));
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Gets the mode of the device.
+     *
+     * @param device Bluetooth device
+     * @return mode of the device. See {@link DeviceMode}.
+     */
+    @DeviceMode
+    public int getDeviceMode(@NonNull BluetoothDevice device) {
+        final int defaultValue = DeviceMode.MODE_INVALID;
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to HearingAidService");
+            return defaultValue;
+        }
+
+        try {
+            Method method = mService.getClass().getDeclaredMethod("getDeviceModeInternal",
+                    BluetoothDevice.class);
+            method.setAccessible(true);
+            return (int) method.invoke(mService, device);
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+            Log.e(TAG, "fail to get getDeviceModeInternal\n" + e.toString() + "\n"
+                    + Log.getStackTraceString(new Throwable()));
+
+            return defaultValue;
+        }
+    }
+
     public String toString() {
         return NAME;
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 0619986..58944f6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -340,6 +340,11 @@
             if (getHearingAidProfile() != null &&
                 mProfile instanceof HearingAidProfile &&
                 (newState == BluetoothProfile.STATE_CONNECTED)) {
+                final int side = getHearingAidProfile().getDeviceSide(cachedDevice.getDevice());
+                final int mode = getHearingAidProfile().getDeviceMode(cachedDevice.getDevice());
+                cachedDevice.setDeviceSide(side);
+                cachedDevice.setDeviceMode(mode);
+
                 // Check if the HiSyncID has being initialized
                 if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
                     long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice());
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 55d125e..be2a55e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -78,6 +78,7 @@
     @Mock
     private BluetoothDevice mSubDevice;
     private CachedBluetoothDevice mCachedDevice;
+    private CachedBluetoothDevice mSubCachedDevice;
     private AudioManager mAudioManager;
     private Context mContext;
     private int mBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
@@ -95,7 +96,9 @@
         when(mPanProfile.isProfileReady()).thenReturn(true);
         when(mHearingAidProfile.isProfileReady()).thenReturn(true);
         mCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mDevice));
+        mSubCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mSubDevice));
         doAnswer((invocation) -> mBatteryLevel).when(mCachedDevice).getBatteryLevel();
+        doAnswer((invocation) -> mBatteryLevel).when(mSubCachedDevice).getBatteryLevel();
     }
 
     @Test
@@ -351,8 +354,9 @@
         assertThat(mCachedDevice.getConnectionSummary()).isNull();
 
         // Set device as Active for Hearing Aid and test connection state summary
+        mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT);
         mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
-        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active");
+        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, left only");
 
         // Set Hearing Aid profile to be disconnected and test connection state summary
         mCachedDevice.onActiveDeviceChanged(false, BluetoothProfile.HEARING_AID);
@@ -390,17 +394,36 @@
     }
 
     @Test
-    public void getConnectionSummary_testHearingAidInCall_returnActive() {
+    public void getConnectionSummary_testHearingAidRightEarInCall_returnActiveRightEar() {
         // Arrange:
-        //   1. Profile:       {HEARING_AID, Connected, Active}
+        //   1. Profile:       {HEARING_AID, Connected, Active, Right ear}
         //   2. Audio Manager: In Call
         updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+        mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT);
         mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
         mAudioManager.setMode(AudioManager.MODE_IN_CALL);
 
         // Act & Assert:
         //   Get "Active" result without Battery Level.
-        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active");
+        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, right only");
+    }
+
+    @Test
+    public void getConnectionSummary_testHearingAidBothEarInCall_returnActiveBothEar() {
+        // Arrange:
+        //   1. Profile:       {HEARING_AID, Connected, Active, Both ear}
+        //   2. Audio Manager: In Call
+        mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT);
+        updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+        mSubCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT);
+        updateSubDeviceProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+        mCachedDevice.setSubDevice(mSubCachedDevice);
+        mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
+        mAudioManager.setMode(AudioManager.MODE_IN_CALL);
+
+        // Act & Assert:
+        //   Get "Active" result without Battery Level.
+        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, left and right");
     }
 
     @Test
@@ -925,39 +948,41 @@
         mCachedDevice.onProfileStateChanged(profile, status);
     }
 
+    private void updateSubDeviceProfileStatus(LocalBluetoothProfile profile, int status) {
+        doReturn(status).when(profile).getConnectionStatus(mSubDevice);
+        mSubCachedDevice.onProfileStateChanged(profile, status);
+    }
+
     @Test
     public void getSubDevice_setSubDevice() {
-        CachedBluetoothDevice subCachedDevice = new CachedBluetoothDevice(mContext, mProfileManager,
-                mSubDevice);
-        mCachedDevice.setSubDevice(subCachedDevice);
+        mCachedDevice.setSubDevice(mSubCachedDevice);
 
-        assertThat(mCachedDevice.getSubDevice()).isEqualTo(subCachedDevice);
+        assertThat(mCachedDevice.getSubDevice()).isEqualTo(mSubCachedDevice);
     }
 
     @Test
     public void switchSubDeviceContent() {
-        CachedBluetoothDevice subCachedDevice = new CachedBluetoothDevice(mContext, mProfileManager,
-                mSubDevice);
+
         mCachedDevice.mRssi = RSSI_1;
         mCachedDevice.mJustDiscovered = JUSTDISCOVERED_1;
-        subCachedDevice.mRssi = RSSI_2;
-        subCachedDevice.mJustDiscovered = JUSTDISCOVERED_2;
-        mCachedDevice.setSubDevice(subCachedDevice);
+        mSubCachedDevice.mRssi = RSSI_2;
+        mSubCachedDevice.mJustDiscovered = JUSTDISCOVERED_2;
+        mCachedDevice.setSubDevice(mSubCachedDevice);
 
         assertThat(mCachedDevice.mRssi).isEqualTo(RSSI_1);
         assertThat(mCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
         assertThat(mCachedDevice.mDevice).isEqualTo(mDevice);
-        assertThat(subCachedDevice.mRssi).isEqualTo(RSSI_2);
-        assertThat(subCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
-        assertThat(subCachedDevice.mDevice).isEqualTo(mSubDevice);
+        assertThat(mSubCachedDevice.mRssi).isEqualTo(RSSI_2);
+        assertThat(mSubCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
+        assertThat(mSubCachedDevice.mDevice).isEqualTo(mSubDevice);
         mCachedDevice.switchSubDeviceContent();
 
         assertThat(mCachedDevice.mRssi).isEqualTo(RSSI_2);
         assertThat(mCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
         assertThat(mCachedDevice.mDevice).isEqualTo(mSubDevice);
-        assertThat(subCachedDevice.mRssi).isEqualTo(RSSI_1);
-        assertThat(subCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
-        assertThat(subCachedDevice.mDevice).isEqualTo(mDevice);
+        assertThat(mSubCachedDevice.mRssi).isEqualTo(RSSI_1);
+        assertThat(mSubCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
+        assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
     }
 
     @Test
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2c3d947..7010a28 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -804,6 +804,14 @@
     <!-- Message shown when non-bypass face authentication succeeds and UDFPS is supported. Provides extra instructions for how the user can enter their device [CHAR LIMIT=60] -->
     <string name="keyguard_face_successful_unlock_press">Unlocked by face. Press the unlock icon to open.</string>
 
+    <!-- Messages shown when users press outside of udfps region during -->
+    <string-array name="udfps_accessibility_touch_hints">
+        <item>Move left</item>
+        <item>Move down</item>
+        <item>Move right</item>
+        <item>Move up</item>
+    </string-array>
+
     <!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
     <string name="keyguard_retry">Swipe up to try again</string>
 
@@ -2271,6 +2279,8 @@
     <string name="media_output_dialog_unknown_launch_app_name">Unknown app</string>
     <!-- Button text for stopping casting [CHAR LIMIT=60] -->
     <string name="media_output_dialog_button_stop_casting">Stop casting</string>
+    <!-- Accessibility text describing purpose of media output dialog. [CHAR LIMIT=NONE] -->
+    <string name="media_output_dialog_accessibility_title">Available devices for audio output.</string>
 
     <!-- Media Output Broadcast Dialog -->
     <!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 5c9f5db..2cc5ccdc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -32,6 +32,7 @@
 import android.content.Context;
 import android.graphics.Insets;
 import android.graphics.Rect;
+import android.os.Trace;
 import android.util.AttributeSet;
 import android.view.WindowInsetsAnimationControlListener;
 import android.view.WindowInsetsAnimationController;
@@ -44,6 +45,7 @@
 
 import com.android.internal.widget.LockscreenCredential;
 import com.android.internal.widget.TextViewInputDisabler;
+import com.android.systemui.DejankUtils;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 /**
@@ -194,9 +196,17 @@
 
                             @Override
                             public void onAnimationEnd(Animator animation) {
-                                controller.finish(false);
-                                runOnFinishImeAnimationRunnable();
-                                finishRunnable.run();
+                                // Run this in the next frame since it results in a slow binder call
+                                // to InputMethodManager#hideSoftInput()
+                                DejankUtils.postAfterTraversal(() -> {
+                                    Trace.beginSection("KeyguardPasswordView#onAnimationEnd");
+                                    // // TODO(b/230620476): Make hideSoftInput oneway
+                                    // controller.finish() eventually calls hideSoftInput
+                                    controller.finish(false);
+                                    runOnFinishImeAnimationRunnable();
+                                    finishRunnable.run();
+                                    Trace.endSection();
+                                });
                             }
                         });
                         anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 5cfbdb0..742c65c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -192,4 +192,9 @@
      * Called on touches outside of the view if listenForTouchesOutsideView returns true
      */
     open fun onTouchOutsideView() {}
+
+    /**
+     * Called when a view should announce an accessibility event.
+     */
+    open fun doAnnounceForAccessibility(str: String) {}
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 9febaa0..88e467f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -381,6 +381,27 @@
                 && mOverlayParams.getSensorBounds().contains((int) x, (int) y);
     }
 
+    private Point getTouchInNativeCoordinates(@NonNull MotionEvent event, int idx) {
+        Point portraitTouch = new Point(
+                (int) event.getRawX(idx),
+                (int) event.getRawY(idx)
+        );
+        final int rot = mOverlayParams.getRotation();
+        if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
+            RotationUtils.rotatePoint(portraitTouch,
+                    RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
+                    mOverlayParams.getLogicalDisplayWidth(),
+                    mOverlayParams.getLogicalDisplayHeight()
+            );
+        }
+
+        // Scale the coordinates to native resolution.
+        final float scale = mOverlayParams.getScaleFactor();
+        portraitTouch.x = (int) (portraitTouch.x / scale);
+        portraitTouch.y = (int) (portraitTouch.y / scale);
+        return portraitTouch;
+    }
+
     @VisibleForTesting
     boolean onTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) {
         if (mOverlay == null) {
@@ -434,6 +455,7 @@
                     mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */);
                     mAttemptedToDismissKeyguard = true;
                 }
+
                 Trace.endSection();
                 break;
 
@@ -457,6 +479,8 @@
                         mAttemptedToDismissKeyguard = true;
                         break;
                     }
+                    // Map the touch to portrait mode if the device is in landscape mode.
+                    final Point scaledTouch = getTouchInNativeCoordinates(event, idx);
                     if (actionMoveWithinSensorArea) {
                         if (mVelocityTracker == null) {
                             // touches could be injected, so the velocity tracker may not have
@@ -477,28 +501,13 @@
                         final long sinceLastLog = mSystemClock.elapsedRealtime() - mTouchLogTime;
                         if (!isIlluminationRequested && !mAcquiredReceived
                                 && !exceedsVelocityThreshold) {
-                            // Map the touch to portrait mode if the device is in landscape mode.
-                            Point portraitTouch = new Point(
-                                    (int) event.getRawX(idx),
-                                    (int) event.getRawY(idx)
-                            );
-                            final int rot = mOverlayParams.getRotation();
-                            if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
-                                RotationUtils.rotatePoint(portraitTouch,
-                                        RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
-                                        mOverlayParams.getLogicalDisplayWidth(),
-                                        mOverlayParams.getLogicalDisplayHeight()
-                                );
-                            }
 
-                            // Scale the coordinates to native resolution.
                             final float scale = mOverlayParams.getScaleFactor();
-                            int scaledX = (int) (portraitTouch.x / scale);
-                            int scaledY = (int) (portraitTouch.y / scale);
                             float scaledMinor = minor / scale;
                             float scaledMajor = major / scale;
 
-                            onFingerDown(requestId, scaledX, scaledY, scaledMinor, scaledMajor);
+                            onFingerDown(requestId, scaledTouch.x, scaledTouch.y, scaledMinor,
+                                    scaledMajor);
                             Log.v(TAG, "onTouch | finger down: " + touchInfo);
                             mTouchLogTime = mSystemClock.elapsedRealtime();
                             mPowerManager.userActivity(mSystemClock.uptimeMillis(),
@@ -511,6 +520,24 @@
                     } else {
                         Log.v(TAG, "onTouch | finger outside");
                         onFingerUp(requestId, udfpsView);
+                        // Maybe announce for accessibility.
+                        mFgExecutor.execute(() -> {
+                            if (mOverlay == null) {
+                                Log.e(TAG, "touch outside sensor area received"
+                                        + "but serverRequest is null");
+                                return;
+                            }
+                            // Scale the coordinates to native resolution.
+                            final float scale = mOverlayParams.getScaleFactor();
+                            final float scaledSensorX =
+                                    mOverlayParams.getSensorBounds().centerX() / scale;
+                            final float scaledSensorY =
+                                    mOverlayParams.getSensorBounds().centerY() / scale;
+
+                            mOverlay.onTouchOutsideOfSensorArea(
+                                    scaledTouch.x, scaledTouch.y, scaledSensorX, scaledSensorY,
+                                    mOverlayParams.getRotation());
+                        });
                     }
                 }
                 Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index faa93a5..37db2bd 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -130,6 +130,8 @@
     val animationViewController: UdfpsAnimationViewController<*>?
         get() = overlayView?.animationViewController
 
+    private var touchExplorationEnabled = false
+
     /** Show the overlay or return false and do nothing if it is already showing. */
     @SuppressLint("ClickableViewAccessibility")
     fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
@@ -154,14 +156,16 @@
                     }
 
                     windowManager.addView(this, coreLayoutParams.updateDimensions(animation))
-
+                    touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
                     overlayTouchListener = TouchExplorationStateChangeListener {
                         if (accessibilityManager.isTouchExplorationEnabled) {
                             setOnHoverListener { v, event -> onTouch(v, event, true) }
                             setOnTouchListener(null)
+                            touchExplorationEnabled = true
                         } else {
                             setOnHoverListener(null)
                             setOnTouchListener { v, event -> onTouch(v, event, true) }
+                            touchExplorationEnabled = false
                         }
                     }
                     accessibilityManager.addTouchExplorationStateChangeListener(
@@ -179,7 +183,7 @@
         return false
     }
 
-    private fun inflateUdfpsAnimation(
+    fun inflateUdfpsAnimation(
         view: UdfpsView,
         controller: UdfpsController
     ): UdfpsAnimationViewController<*>? {
@@ -278,6 +282,88 @@
         enrollHelper?.onEnrollmentHelp()
     }
 
+    /**
+     * This function computes the angle of touch relative to the sensor and maps
+     * the angle to a list of help messages which are announced if accessibility is enabled.
+     *
+     */
+    fun onTouchOutsideOfSensorArea(
+        touchX: Float,
+        touchY: Float,
+        sensorX: Float,
+        sensorY: Float,
+        rotation: Int
+    ) {
+
+        if (!touchExplorationEnabled) {
+            return
+        }
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+        if (touchHints.size != 4) {
+            Log.e(TAG, "expected exactly 4 touch hints, got $touchHints.size?")
+            return
+        }
+        val theStr = onTouchOutsideOfSensorAreaImpl(touchX, touchY, sensorX, sensorY, rotation)
+        Log.v(TAG, "Announcing touch outside : " + theStr)
+        animationViewController?.doAnnounceForAccessibility(theStr)
+    }
+
+    /**
+     * This function computes the angle of touch relative to the sensor and maps
+     * the angle to a list of help messages which are announced if accessibility is enabled.
+     *
+     * There are 4 quadrants of the circle (90 degree arcs)
+     *
+     * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left"
+     * [45,  135)            -> touchHints[1] = "Move Fingerprint down"
+     * And so on.
+     */
+    fun onTouchOutsideOfSensorAreaImpl(
+        touchX: Float,
+        touchY: Float,
+        sensorX: Float,
+        sensorY: Float,
+        rotation: Int
+    ): String {
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+
+        val xRelativeToSensor = touchX - sensorX
+        // Touch coordinates are with respect to the upper left corner, so reverse
+        // this calculation
+        val yRelativeToSensor = sensorY - touchY
+
+        var angleInRad =
+            Math.atan2(yRelativeToSensor.toDouble(), xRelativeToSensor.toDouble())
+        // If the radians are negative, that means we are counting clockwise.
+        // So we need to add 360 degrees
+        if (angleInRad < 0.0) {
+            angleInRad += 2.0 * Math.PI
+        }
+        // rad to deg conversion
+        val degrees = Math.toDegrees(angleInRad)
+
+        val degreesPerBucket = 360.0 / touchHints.size
+        val halfBucketDegrees = degreesPerBucket / 2.0
+        // The mapping should be as follows
+        // [315, 360] && [0, 45] -> 0
+        // [45, 135]             -> 1
+        var index = (((degrees + halfBucketDegrees) % 360) / degreesPerBucket).toInt()
+        index %= touchHints.size
+
+        // A rotation of 90 degrees corresponds to increasing the index by 1.
+        if (rotation == Surface.ROTATION_90) {
+            index = (index + 1) % touchHints.size
+        }
+
+        if (rotation == Surface.ROTATION_270) {
+            index = (index + 3) % touchHints.size
+        }
+
+        return touchHints[index]
+    }
+
     /** Cancel this request. */
     fun cancel() {
         try {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index 2ed60e5..0b7bdde 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -99,4 +99,10 @@
     public int getPaddingY() {
         return mEnrollProgressBarRadius;
     }
+
+    @Override
+    public void doAnnounceForAccessibility(String str) {
+        mView.announceForAccessibility(str);
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
index 25effec..db446c3 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
@@ -19,13 +19,14 @@
 import android.os.Trace
 import android.util.Log
 import com.android.systemui.log.dagger.LogModule
+import com.android.systemui.util.collection.RingBuffer
 import java.io.PrintWriter
 import java.text.SimpleDateFormat
-import java.util.ArrayDeque
 import java.util.Locale
 import java.util.concurrent.ArrayBlockingQueue
 import java.util.concurrent.BlockingQueue
 import kotlin.concurrent.thread
+import kotlin.math.max
 
 /**
  * A simple ring buffer of recyclable log messages
@@ -63,29 +64,22 @@
  *
  * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory].
  *
- * @param name The name of this buffer
- * @param maxLogs The maximum number of messages to keep in memory at any one time, including the
- * unused pool. Must be >= [poolSize].
- * @param poolSize The maximum amount that the size of the buffer is allowed to flex in response to
- * sequential calls to [document] that aren't immediately followed by a matching call to [push].
+ * @param name The name of this buffer, printed when the buffer is dumped and in some other
+ * situations.
+ * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
+ * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches
+ * the maximum, it behaves like a ring buffer.
  */
 class LogBuffer @JvmOverloads constructor(
     private val name: String,
-    private val maxLogs: Int,
-    private val poolSize: Int,
+    private val maxSize: Int,
     private val logcatEchoTracker: LogcatEchoTracker,
     private val systrace: Boolean = true
 ) {
-    init {
-        if (maxLogs < poolSize) {
-            throw IllegalArgumentException("maxLogs must be greater than or equal to poolSize, " +
-                    "but maxLogs=$maxLogs < $poolSize=poolSize")
-        }
-    }
+    private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
 
-    private val buffer: ArrayDeque<LogMessageImpl> = ArrayDeque()
-    private val echoMessageQueue: BlockingQueue<LogMessageImpl>? =
-            if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(poolSize) else null
+    private val echoMessageQueue: BlockingQueue<LogMessage>? =
+            if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
 
     init {
         if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
@@ -104,6 +98,9 @@
     var frozen = false
         private set
 
+    private val mutable
+        get() = !frozen && maxSize > 0
+
     /**
      * Logs a message to the log buffer
      *
@@ -138,34 +135,19 @@
         initializer: LogMessage.() -> Unit,
         noinline printer: LogMessage.() -> String
     ) {
-        if (!frozen) {
-            val message = obtain(tag, level, printer)
-            initializer(message)
-            push(message)
-        }
-    }
-
-    /**
-     * Same as [log], but doesn't push the message to the buffer. Useful if you need to supply a
-     * "reason" for doing something (the thing you supply the reason to will presumably call [push]
-     * on that message at some point).
-     */
-    inline fun document(
-        tag: String,
-        level: LogLevel,
-        initializer: LogMessage.() -> Unit,
-        noinline printer: LogMessage.() -> String
-    ): LogMessage {
         val message = obtain(tag, level, printer)
         initializer(message)
-        return message
+        commit(message)
     }
 
     /**
-     * Obtains an instance of [LogMessageImpl], usually from the object pool. If the pool has been
-     * exhausted, creates a new instance.
+     * You should call [log] instead of this method.
      *
-     * In general, you should call [log] or [document] instead of this method.
+     * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size,
+     * grows the buffer by one.
+     *
+     * After calling [obtain], the message will now be at the end of the buffer. The caller must
+     * store any relevant data on the message and then call [commit].
      */
     @Synchronized
     fun obtain(
@@ -173,28 +155,26 @@
         level: LogLevel,
         printer: (LogMessage) -> String
     ): LogMessageImpl {
-        val message = when {
-            frozen -> LogMessageImpl.create()
-            buffer.size > maxLogs - poolSize -> buffer.removeFirst()
-            else -> LogMessageImpl.create()
+        if (!mutable) {
+            return FROZEN_MESSAGE
         }
+        val message = buffer.advance()
         message.reset(tag, level, System.currentTimeMillis(), printer)
         return message
     }
 
     /**
-     * Pushes a message into buffer, possibly evicting an older message if the buffer is full.
+     * You should call [log] instead of this method.
+     *
+     * After acquiring a message via [obtain], call this method to signal to the buffer that you
+     * have finished filling in its data fields. The message will be echoed to logcat if
+     * necessary.
      */
     @Synchronized
-    fun push(message: LogMessage) {
-        if (frozen) {
+    fun commit(message: LogMessage) {
+        if (!mutable) {
             return
         }
-        if (buffer.size == maxLogs) {
-            Log.e(TAG, "LogBuffer $name has exceeded its pool size")
-            buffer.removeFirst()
-        }
-        buffer.add(message as LogMessageImpl)
         // Log in the background thread only if echoMessageQueue exists and has capacity (checking
         // capacity avoids the possibility of blocking this thread)
         if (echoMessageQueue != null && echoMessageQueue.remainingCapacity() > 0) {
@@ -210,7 +190,7 @@
     }
 
     /** Sends message to echo after determining whether to use Logcat and/or systrace. */
-    private fun echoToDesiredEndpoints(message: LogMessageImpl) {
+    private fun echoToDesiredEndpoints(message: LogMessage) {
         val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) ||
                 logcatEchoTracker.isTagLoggable(message.tag, message.level)
         echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
@@ -219,19 +199,17 @@
     /** Converts the entire buffer to a newline-delimited string */
     @Synchronized
     fun dump(pw: PrintWriter, tailLength: Int) {
-        val start = if (tailLength <= 0) { 0 } else { buffer.size - tailLength }
+        val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) }
 
-        for ((i, message) in buffer.withIndex()) {
-            if (i >= start) {
-                dumpMessage(message, pw)
-            }
+        for (i in iterationStart until buffer.size) {
+            dumpMessage(buffer[i], pw)
         }
     }
 
     /**
-     * "Freezes" the contents of the buffer, making them immutable until [unfreeze] is called.
-     * Calls to [log], [document], [obtain], and [push] will not affect the buffer and will return
-     * dummy values if necessary.
+     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called.
+     * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy
+     * values if necessary.
      */
     @Synchronized
     fun freeze() {
@@ -293,3 +271,4 @@
 
 private const val TAG = "LogBuffer"
 private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+private val FROZEN_MESSAGE = LogMessageImpl.create()
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
index cbfca25..5651399 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
@@ -27,22 +27,21 @@
     private val logcatEchoTracker: LogcatEchoTracker
 ) {
     /* limit the size of maxPoolSize for low ram (Go) devices */
-    private fun poolLimit(maxPoolSize_requested: Int): Int {
-        if (ActivityManager.isLowRamDeviceStatic()) {
-            return minOf(maxPoolSize_requested, 20) /* low ram max log size*/
+    private fun adjustMaxSize(requestedMaxSize: Int): Int {
+        return if (ActivityManager.isLowRamDeviceStatic()) {
+            minOf(requestedMaxSize, 20) /* low ram max log size*/
         } else {
-            return maxPoolSize_requested
+            requestedMaxSize
         }
     }
 
     @JvmOverloads
     fun create(
         name: String,
-        maxPoolSize: Int,
-        flexSize: Int = 10,
+        maxSize: Int,
         systrace: Boolean = true
     ): LogBuffer {
-        val buffer = LogBuffer(name, poolLimit(maxPoolSize), flexSize, logcatEchoTracker, systrace)
+        val buffer = LogBuffer(name, adjustMaxSize(maxSize), logcatEchoTracker, systrace)
         dumpManager.registerBuffer(name, buffer)
         return buffer
     }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
index 91734cc..40b0cdc 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
@@ -60,7 +60,7 @@
                 Settings.Global.getUriFor(BUFFER_PATH),
                 true,
                 object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri) {
+                    override fun onChange(selfChange: Boolean, uri: Uri?) {
                         super.onChange(selfChange, uri)
                         cachedBufferLevels.clear()
                     }
@@ -70,7 +70,7 @@
                 Settings.Global.getUriFor(TAG_PATH),
                 true,
                 object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri) {
+                    override fun onChange(selfChange: Boolean, uri: Uri?) {
                         super.onChange(selfChange, uri)
                         cachedTagLevels.clear()
                     }
@@ -110,7 +110,7 @@
     }
 
     private fun parseProp(propValue: String?): LogLevel {
-        return when (propValue?.toLowerCase()) {
+        return when (propValue?.lowercase()) {
             "verbose" -> LogLevel.VERBOSE
             "v" -> LogLevel.VERBOSE
             "debug" -> LogLevel.DEBUG
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 1e7a292..eff025f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -49,8 +49,7 @@
     @SysUISingleton
     @NotificationLog
     public static LogBuffer provideNotificationsLogBuffer(LogBufferFactory factory) {
-        return factory.create("NotifLog", 1000 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("NotifLog", 1000 /* maxSize */, false /* systrace */);
     }
 
     /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -74,8 +73,7 @@
     @SysUISingleton
     @NotificationSectionLog
     public static LogBuffer provideNotificationSectionLogBuffer(LogBufferFactory factory) {
-        return factory.create("NotifSectionLog", 1000 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("NotifSectionLog", 1000 /* maxSize */, false /* systrace */);
     }
 
     /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -91,8 +89,7 @@
     @SysUISingleton
     @QSLog
     public static LogBuffer provideQuickSettingsLogBuffer(LogBufferFactory factory) {
-        return factory.create("QSLog", 500 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("QSLog", 500 /* maxSize */, false /* systrace */);
     }
 
     /** Provides a logging buffer for {@link com.android.systemui.broadcast.BroadcastDispatcher} */
@@ -100,8 +97,8 @@
     @SysUISingleton
     @BroadcastDispatcherLog
     public static LogBuffer provideBroadcastDispatcherLogBuffer(LogBufferFactory factory) {
-        return factory.create("BroadcastDispatcherLog", 500 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("BroadcastDispatcherLog", 500 /* maxSize */,
+                false /* systrace */);
     }
 
     /** Provides a logging buffer for all logs related to Toasts shown by SystemUI. */
@@ -139,8 +136,8 @@
     @SysUISingleton
     @QSFragmentDisableLog
     public static LogBuffer provideQSFragmentDisableLogBuffer(LogBufferFactory factory) {
-        return factory.create("QSFragmentDisableFlagsLog", 10 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("QSFragmentDisableFlagsLog", 10 /* maxSize */,
+                false /* systrace */);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 1a727f8..fa6db01 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -227,9 +227,7 @@
         lp.setFitInsetsIgnoringVisibility(true);
         window.setAttributes(lp);
         window.setContentView(mDialogView);
-        // Sets window to a blank string to avoid talkback announce app label first when pop up,
-        // which doesn't make sense.
-        window.setTitle(EMPTY_TITLE);
+        window.setTitle(mContext.getString(R.string.media_output_dialog_accessibility_title));
 
         mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
         mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
index 7530681..f68e042 100644
--- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
@@ -58,6 +58,7 @@
     private Drawable mDrawable;
     private PorterDuffColorFilter mColorFilter;
     private int mTintColor;
+    private boolean mBlendWithMainColor = true;
     private Runnable mChangeRunnable;
     private Executor mChangeRunnableExecutor;
     private Executor mExecutor;
@@ -192,6 +193,19 @@
     }
 
     /**
+     * The call to {@link #setTint} will blend with the main color, with the amount
+     * determined by the alpha of the tint. Set to false to avoid this blend.
+     */
+    public void setBlendWithMainColor(boolean blend) {
+        mBlendWithMainColor = blend;
+    }
+
+    /** @return true if blending tint color with main color */
+    public boolean shouldBlendWithMainColor() {
+        return mBlendWithMainColor;
+    }
+
+    /**
      * Tints this view, optionally animating it.
      * @param color The color.
      * @param animated If we should animate.
@@ -211,8 +225,11 @@
             // Optimization to blend colors and avoid a color filter
             ScrimDrawable drawable = (ScrimDrawable) mDrawable;
             float tintAmount = Color.alpha(mTintColor) / 255f;
-            int mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor,
-                    tintAmount);
+
+            int mainTinted = mTintColor;
+            if (mBlendWithMainColor) {
+                mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor, tintAmount);
+            }
             drawable.setColor(mainTinted, animated);
         } else {
             boolean hasAlpha = Color.alpha(mTintColor) != 0;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt
index 0623507..d44a569 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt
@@ -14,9 +14,9 @@
 package com.android.systemui.statusbar.phone
 
 import android.view.MotionEvent
-import com.android.internal.util.RingBuffer
 import com.android.systemui.dump.DumpsysTableLogger
 import com.android.systemui.dump.Row
+import com.android.systemui.util.collection.RingBuffer
 import java.text.SimpleDateFormat
 import java.util.Locale
 
@@ -67,16 +67,9 @@
      * Do not use [append] to add new elements. Instead use [insert], as it will recycle if
      * necessary.
      */
-    class Buffer(
-        capacity: Int
-    ) : RingBuffer<NPVCDownEventState>(NPVCDownEventState::class.java, capacity) {
-        override fun append(t: NPVCDownEventState?) {
-            throw UnsupportedOperationException("Not supported, use insert instead")
-        }
+    class Buffer(capacity: Int) {
 
-        override fun createNewItem(): NPVCDownEventState {
-            return NPVCDownEventState()
-        }
+        private val buffer = RingBuffer(capacity) { NPVCDownEventState() }
 
         /**
          * Insert a new element in the buffer.
@@ -94,7 +87,7 @@
             touchSlopExceededBeforeDown: Boolean,
             lastEventSynthesized: Boolean
         ) {
-            nextSlot.apply {
+            buffer.advance().apply {
                 this.timeStamp = timeStamp
                 this.x = x
                 this.y = y
@@ -115,7 +108,7 @@
          * @see NPVCDownEventState.asStringList
          */
         fun toList(): List<Row> {
-            return toArray().map { it.asStringList }
+            return buffer.asSequence().map { it.asStringList }.toList()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index f9e17da..0e77866 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -402,6 +402,8 @@
         mScrimBehind.setFocusable(!state.isLowPowerState());
         mNotificationsScrim.setFocusable(!state.isLowPowerState());
 
+        mScrimInFront.setBlendWithMainColor(state.shouldBlendWithMainColor());
+
         // Cancel blanking transitions that were pending before we requested a new state
         if (mPendingFrameCallback != null) {
             mScrimBehind.removeCallbacks(mPendingFrameCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 47b7058..4a5f712 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -203,6 +203,11 @@
         public boolean isLowPowerState() {
             return true;
         }
+
+        @Override
+        public boolean shouldBlendWithMainColor() {
+            return false;
+        }
     },
 
     /**
@@ -325,6 +330,13 @@
     public void prepare(ScrimState previousState) {
     }
 
+    /**
+     * Whether a particular state should enable blending with extracted theme colors.
+     */
+    public boolean shouldBlendWithMainColor() {
+        return true;
+    }
+
     public float getFrontAlpha() {
         return mFrontAlpha;
     }
@@ -422,4 +434,4 @@
     public void setClipQsScrim(boolean clipsQsScrim) {
         mClipQsScrim = clipsQsScrim;
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
new file mode 100644
index 0000000..97dc842
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 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.util.collection
+
+import kotlin.math.max
+
+/**
+ * A simple ring buffer implementation
+ *
+ * Use [advance] to get the least recent item in the buffer (and then presumably fill it with
+ * appropriate data). This will cause it to become the most recent item.
+ *
+ * As the buffer is used, it will grow, allocating new instances of T using [factory] until it
+ * reaches [maxSize]. After this point, no new instances will be created. Instead, the "oldest"
+ * instances will be recycled from the back of the buffer and placed at the front.
+ *
+ * @param maxSize The maximum size the buffer can grow to before it begins functioning as a ring.
+ * @param factory A function that creates a fresh instance of T. Used by the buffer while it's
+ * growing to [maxSize].
+ */
+class RingBuffer<T>(
+    private val maxSize: Int,
+    private val factory: () -> T
+) : Iterable<T> {
+
+    private val buffer = MutableList<T?>(maxSize) { null }
+
+    /**
+     * An abstract representation that points to the "end" of the buffer. Increments every time
+     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into
+     * the backing array. Always points to the "next" available slot in the buffer. Before the
+     * buffer has completely filled, the value pointed to will be null. Afterward, it will be the
+     * value at the "beginning" of the buffer.
+     *
+     * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
+     * omega will overflow after a little under three million years of continuous operation.
+     */
+    private var omega: Long = 0
+
+    /**
+     * The number of items currently stored in the buffer. Calls to [advance] will cause this value
+     * to increase by one until it reaches [maxSize].
+     */
+    val size: Int
+        get() = if (omega < maxSize) omega.toInt() else maxSize
+
+    /**
+     * Advances the buffer's position by one and returns the value that is now present at the "end"
+     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item.
+     * Otherwise, reuses the value that was previously at the "beginning" of the buffer.
+     *
+     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that
+     * was previously stored on it.
+     */
+    fun advance(): T {
+        val index = indexOf(omega)
+        omega += 1
+        val entry = buffer[index] ?: factory().also {
+            buffer[index] = it
+        }
+        return entry
+    }
+
+    /**
+     * Returns the value stored at [index], which can range from 0 (the "start", or oldest element
+     * of the buffer) to [size] - 1 (the "end", or newest element of the buffer).
+     */
+    operator fun get(index: Int): T {
+        if (index < 0 || index >= size) {
+            throw IndexOutOfBoundsException("Index $index is out of bounds")
+        }
+
+        // If omega is larger than the maxSize, then the buffer is full, and omega is equivalent
+        // to the "start" of the buffer. If omega is smaller than the maxSize, then the buffer is
+        // not yet full and our start should be 0. However, in modspace, maxSize and 0 are
+        // equivalent, so we can get away with using it as the start value instead.
+        val start = max(omega, maxSize.toLong())
+
+        return buffer[indexOf(start + index)]!!
+    }
+
+    inline fun forEach(action: (T) -> Unit) {
+        for (i in 0 until size) {
+            action(get(i))
+        }
+    }
+
+    override fun iterator(): Iterator<T> {
+        return object : Iterator<T> {
+            private var position: Int = 0
+
+            override fun next(): T {
+                if (position >= size) {
+                    throw NoSuchElementException()
+                }
+                return get(position).also { position += 1 }
+            }
+
+            override fun hasNext(): Boolean {
+                return position < size
+            }
+        }
+    }
+
+    private fun indexOf(position: Long): Int {
+        return (position % maxSize).toInt()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index bfdcbd6..cf0d023 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -32,6 +32,8 @@
 import static android.view.View.VISIBLE;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
+import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
 
 import android.animation.Animator;
@@ -68,6 +70,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
+import android.os.Trace;
 import android.os.VibrationEffect;
 import android.provider.Settings;
 import android.provider.Settings.Global;
@@ -101,9 +104,11 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.view.RotationPolicy;
 import com.android.settingslib.Utils;
 import com.android.systemui.Prefs;
@@ -149,6 +154,13 @@
     private static final int DRAWER_ANIMATION_DURATION_SHORT = 175;
     private static final int DRAWER_ANIMATION_DURATION = 250;
 
+    /** Shows volume dialog show animation. */
+    private static final String TYPE_SHOW = "show";
+    /** Dismiss volume dialog animation.  */
+    private static final String TYPE_DISMISS = "dismiss";
+    /** Volume dialog slider animation. */
+    private static final String TYPE_UPDATE = "update";
+
     private final int mDialogShowAnimationDurationMs;
     private final int mDialogHideAnimationDurationMs;
     private int mDialogWidth;
@@ -258,6 +270,7 @@
     private final boolean mUseBackgroundBlur;
     private Consumer<Boolean> mCrossWindowBlurEnabledListener;
     private BackgroundBlurDrawable mDialogRowsViewBackground;
+    private final InteractionJankMonitor mInteractionJankMonitor;
 
     public VolumeDialogImpl(
             Context context,
@@ -266,7 +279,8 @@
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
             MediaOutputDialogFactory mediaOutputDialogFactory,
-            ActivityStarter activityStarter) {
+            ActivityStarter activityStarter,
+            InteractionJankMonitor interactionJankMonitor) {
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
         mController = volumeDialogController;
@@ -290,6 +304,7 @@
             mContext.getResources().getInteger(R.integer.config_dialogHideAnimationDurationMs);
         mUseBackgroundBlur =
             mContext.getResources().getBoolean(R.bool.config_volumeDialogUseBackgroundBlur);
+        mInteractionJankMonitor = interactionJankMonitor;
 
         if (mUseBackgroundBlur) {
             final int dialogRowsViewColorAboveBlur = mContext.getColor(
@@ -422,6 +437,7 @@
                     .alpha(1)
                     .translationX(0)
                     .setDuration(mDialogShowAnimationDurationMs)
+                    .setListener(getJankListener(getDialogView(), TYPE_SHOW, DIALOG_TIMEOUT_MILLIS))
                     .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
                     .withEndAction(() -> {
                         if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) {
@@ -692,7 +708,7 @@
         final int m = seekBar.getMax();
         final int n = m / 100 - 1;
         final int level = progress == 0 ? 0
-                : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n));
+                : progress == m ? (m / 100) : (1 + (int) ((progress / (float) m) * n));
         return level;
     }
 
@@ -1251,7 +1267,33 @@
         mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget();
     }
 
+    private Animator.AnimatorListener getJankListener(View v, String type, long timeout) {
+        return new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(@NonNull Animator animation) {
+                mInteractionJankMonitor.begin(Builder.withView(CUJ_VOLUME_CONTROL, v).setTag(type)
+                        .setTimeout(timeout));
+            }
+
+            @Override
+            public void onAnimationEnd(@NonNull Animator animation) {
+                mInteractionJankMonitor.end(CUJ_VOLUME_CONTROL);
+            }
+
+            @Override
+            public void onAnimationCancel(@NonNull Animator animation) {
+                mInteractionJankMonitor.cancel(CUJ_VOLUME_CONTROL);
+            }
+
+            @Override
+            public void onAnimationRepeat(@NonNull Animator animation) {
+                // no-op
+            }
+        };
+    }
+
     private void showH(int reason) {
+        Trace.beginSection("VolumeDialogImpl#showH");
         if (D.BUG) Log.d(TAG, "showH r=" + Events.SHOW_REASONS[reason]);
         mHandler.removeMessages(H.SHOW);
         mHandler.removeMessages(H.DISMISS);
@@ -1272,6 +1314,7 @@
         mController.getCaptionsComponentState(false);
         checkODICaptionsTooltip(false);
         updateBackgroundForDrawerClosedAmount();
+        Trace.endSection();
     }
 
     protected void rescheduleTimeoutH() {
@@ -1305,6 +1348,7 @@
     }
 
     protected void dismissH(int reason) {
+        Trace.beginSection("VolumeDialogImpl#dismissH");
         if (D.BUG) {
             Log.d(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason]
                     + " from: " + Debug.getCaller());
@@ -1335,7 +1379,8 @@
                     hideRingerDrawer();
                 }, 50));
         if (!shouldSlideInVolumeTray()) animator.translationX(mDialogView.getWidth() / 2.0f);
-        animator.start();
+        animator.setListener(getJankListener(getDialogView(), TYPE_DISMISS,
+                mDialogHideAnimationDurationMs)).start();
         checkODICaptionsTooltip(true);
         mController.notifyVisible(false);
         synchronized (mSafetyWarningLock) {
@@ -1344,6 +1389,7 @@
                 mSafetyWarning.dismiss();
             }
         }
+        Trace.endSection();
     }
 
     private boolean showActiveStreamOnly() {
@@ -1383,6 +1429,7 @@
     }
 
     private void updateRowsH(final VolumeRow activeRow) {
+        Trace.beginSection("VolumeDialogImpl#updateRowsH");
         if (D.BUG) Log.d(TAG, "updateRowsH");
         if (!mShowing) {
             trimObsoleteH();
@@ -1446,6 +1493,7 @@
         }
 
         updateBackgroundForDrawerClosedAmount();
+        Trace.endSection();
     }
 
     protected void updateRingerH() {
@@ -1730,7 +1778,9 @@
         final boolean enableSlider = !zenMuted;
         final int vlevel = row.ss.muted && (!isRingStream && !zenMuted) ? 0
                 : row.ss.level;
+        Trace.beginSection("VolumeDialogImpl#updateVolumeRowSliderH");
         updateVolumeRowSliderH(row, enableSlider, vlevel);
+        Trace.endSection();
         if (row.number != null) row.number.setText(Integer.toString(vlevel));
     }
 
@@ -1824,6 +1874,8 @@
                 }
                 row.animTargetProgress = newProgress;
                 row.anim.setDuration(UPDATE_ANIMATION_DURATION);
+                row.anim.addListener(
+                        getJankListener(row.view, TYPE_UPDATE, UPDATE_ANIMATION_DURATION));
                 row.anim.start();
             } else {
                 // update slider directly to clamped value
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 79aa643..f3855bd 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.media.AudioManager;
 
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -51,7 +52,8 @@
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
             MediaOutputDialogFactory mediaOutputDialogFactory,
-            ActivityStarter activityStarter) {
+            ActivityStarter activityStarter,
+            InteractionJankMonitor interactionJankMonitor) {
         VolumeDialogImpl impl = new VolumeDialogImpl(
                 context,
                 volumeDialogController,
@@ -59,7 +61,8 @@
                 deviceProvisionedController,
                 configurationController,
                 mediaOutputDialogFactory,
-                activityStarter);
+                activityStarter,
+                interactionJankMonitor);
         impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
         impl.setAutomute(true);
         impl.setSilentMode(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 6eba215..431739b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -367,6 +367,73 @@
         assertThat(controllerOverlay.matchesRequestId(REQUEST_ID)).isTrue()
         assertThat(controllerOverlay.matchesRequestId(REQUEST_ID + 1)).isFalse()
     }
+
+    @Test
+    fun testTouchOutsideAreaNoRotation() = withReason(REASON_ENROLL_ENROLLING) {
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+        val rotation = Surface.ROTATION_0
+        // touch at 0 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[0])
+        // touch at 90 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[1])
+        // touch at 180 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[2])
+        // touch at 270 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[3])
+    }
+
+    fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) {
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+        val rotation = Surface.ROTATION_90
+        // touch at 0 degrees -> 90 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[1])
+        // touch at 90 degrees -> 180 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[2])
+        // touch at 180 degrees -> 270 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[3])
+        // touch at 270 degrees -> 0 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[0])
+    }
+
+    fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) {
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+        val rotation = Surface.ROTATION_270
+        // touch at 0 degrees -> 270 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[3])
+        // touch at 90 degrees -> 0 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[0])
+        // touch at 180 degrees -> 90 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[1])
+        // touch at 270 degrees -> 180 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[2])
+    }
 }
 
 private class EnrollListener(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
index 0720bdb..bd029a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
@@ -24,7 +24,7 @@
  * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests.
  */
 fun logcatLogBuffer(name: String = "EchoToLogcatLogBuffer") =
-    LogBuffer(name, 50, 50, LogcatEchoTrackerAlways())
+    LogBuffer(name, 50, LogcatEchoTrackerAlways())
 
 /**
  * A [LogcatEchoTracker] that always allows echoing to the logcat.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
index 6971c63..8cb530c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
@@ -30,7 +30,7 @@
     @Before
     fun setup() {
         logger = LSShadeTransitionLogger(
-                LogBuffer("Test", 10, 10, mock()),
+                LogBuffer("Test", 10, mock()),
                 gestureLogger,
                 displayMetrics)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index dce520c..5f8dda3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -1490,6 +1490,13 @@
         assertAlphaAfterExpansion(mNotificationsScrim, 0f, expansion);
     }
 
+    @Test
+    public void aodStateSetsFrontScrimToNotBlend() {
+        mScrimController.transitionTo(ScrimState.AOD);
+        Assert.assertFalse("Front scrim should not blend with main color",
+                mScrimInFront.shouldBlendWithMainColor());
+    }
+
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
         mScrimController.setRawPanelExpansionFraction(expansion);
         finishAnimationsImmediately();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 98397fb..6abc687 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -379,7 +379,7 @@
                 mCommandQueue,
                 mCarrierConfigTracker,
                 new CollapsedStatusBarFragmentLogger(
-                        new LogBuffer("TEST", 1, 1, mock(LogcatEchoTracker.class)),
+                        new LogBuffer("TEST", 1, mock(LogcatEchoTracker.class)),
                         new DisableFlagsLogger()
                         ),
                 mOperatorNameViewControllerFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
new file mode 100644
index 0000000..5e09b81
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 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.util.collection
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class RingBufferTest : SysuiTestCase() {
+
+    private val buffer = RingBuffer(5) { TestElement() }
+
+    private val history = mutableListOf<TestElement>()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testBarelyFillBuffer() {
+        fillBuffer(5)
+
+        assertEquals(0, buffer[0].id)
+        assertEquals(1, buffer[1].id)
+        assertEquals(2, buffer[2].id)
+        assertEquals(3, buffer[3].id)
+        assertEquals(4, buffer[4].id)
+    }
+
+    @Test
+    fun testPartiallyFillBuffer() {
+        fillBuffer(3)
+
+        assertEquals(3, buffer.size)
+
+        assertEquals(0, buffer[0].id)
+        assertEquals(1, buffer[1].id)
+        assertEquals(2, buffer[2].id)
+
+        assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] }
+        assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] }
+    }
+
+    @Test
+    fun testSpinBuffer() {
+        fillBuffer(277)
+
+        assertEquals(272, buffer[0].id)
+        assertEquals(273, buffer[1].id)
+        assertEquals(274, buffer[2].id)
+        assertEquals(275, buffer[3].id)
+        assertEquals(276, buffer[4].id)
+        assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] }
+
+        assertEquals(5, buffer.size)
+    }
+
+    @Test
+    fun testElementsAreRecycled() {
+        fillBuffer(23)
+
+        assertSame(history[4], buffer[1])
+        assertSame(history[9], buffer[1])
+        assertSame(history[14], buffer[1])
+        assertSame(history[19], buffer[1])
+    }
+
+    @Test
+    fun testIterator() {
+        fillBuffer(13)
+
+        val iterator = buffer.iterator()
+
+        for (i in 0 until 5) {
+            assertEquals(history[8 + i], iterator.next())
+        }
+        assertFalse(iterator.hasNext())
+        assertThrows(NoSuchElementException::class.java) { iterator.next() }
+    }
+
+    @Test
+    fun testForEach() {
+        fillBuffer(13)
+        var i = 8
+
+        buffer.forEach {
+            assertEquals(history[i], it)
+            i++
+        }
+        assertEquals(13, i)
+    }
+
+    private fun fillBuffer(count: Int) {
+        for (i in 0 until count) {
+            val elem = buffer.advance()
+            elem.id = history.size
+            history.add(elem)
+        }
+    }
+}
+
+private class TestElement(var id: Int = 0) {
+    override fun toString(): String {
+        return "{TestElement $id}"
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 9493456..312db2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -38,6 +38,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
@@ -85,6 +86,8 @@
     MediaOutputDialogFactory mMediaOutputDialogFactory;
     @Mock
     ActivityStarter mActivityStarter;
+    @Mock
+    InteractionJankMonitor mInteractionJankMonitor;
 
     @Before
     public void setup() throws Exception {
@@ -99,7 +102,8 @@
                 mDeviceProvisionedController,
                 mConfigurationController,
                 mMediaOutputDialogFactory,
-                mActivityStarter);
+                mActivityStarter,
+                mInteractionJankMonitor);
         mDialog.init(0, null);
         State state = createShellState();
         mDialog.onStateChangedH(state);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 9646edf..9ca6bb0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -69,7 +69,7 @@
                 statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
                 bubbleLogger, taskStackListener, shellTaskOrganizer, positioner, displayController,
                 oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
-                taskViewTransitions, syncQueue);
+                new SyncExecutor(), taskViewTransitions, syncQueue);
         setInflateSynchronously(true);
         initialize();
     }
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 7a5fa62..570e4e6 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -182,7 +182,7 @@
         // 2b.1. Populate the request with required info.
         request.setPackageName(packageName);
         request.setUserId(userId);
-        request.setSkipPrompt(mayAssociateWithoutPrompt(request, packageName, userId));
+        request.setSkipPrompt(mayAssociateWithoutPrompt(packageName, userId));
 
         // 2b.2. Prepare extras and create an Intent.
         final Bundle extras = new Bundle();
@@ -321,18 +321,7 @@
         }
     };
 
-    private boolean mayAssociateWithoutPrompt(@NonNull AssociationRequest request,
-            @NonNull String packageName, @UserIdInt int userId) {
-        final String deviceProfile = request.getDeviceProfile();
-        if (deviceProfile != null) {
-            final boolean isRoleHolder = Binder.withCleanCallingIdentity(
-                    () -> isRoleHolder(mContext, userId, packageName, deviceProfile));
-            if (isRoleHolder) {
-                // Don't need to collect user's consent since app already holds the role.
-                return true;
-            }
-        }
-
+    private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) {
         // Below we check if the requesting package is allowlisted (usually by the OEM) for creating
         // CDM associations without user confirmation (prompt).
         // For this we'll check to config arrays:
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 8622ef0..0bfe282 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -78,6 +78,7 @@
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.ShellCallback;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArraySet;
@@ -121,8 +122,10 @@
 
     private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
     private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
+    private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW =
+            "debug.cdm.cdmservice.removal_time_window";
 
-    private static final long ASSOCIATION_CLEAN_UP_TIME_WINDOW = DAYS.toMillis(3 * 30); // 3 months
+    private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
 
     private PersistentDataStore mPersistentStore;
     private final PersistUserStateHandler mUserPersistenceHandler;
@@ -211,8 +214,8 @@
             mPackageMonitor.register(context, FgThread.get().getLooper(), UserHandle.ALL, true);
             mDevicePresenceMonitor.init(context);
         } else if (phase == PHASE_BOOT_COMPLETED) {
-            // Run the Association CleanUp job service daily.
-            AssociationCleanUpService.schedule(getContext());
+            // Run the Inactive Association Removal job service daily.
+            InactiveAssociationsRemovalService.schedule(getContext());
         }
     }
 
@@ -410,17 +413,20 @@
         mCompanionAppController.onPackagesChanged(userId);
     }
 
-    // Revoke associations if the selfManaged companion device does not connect for 3
-    // months for specific profile.
-    private void associationCleanUp(String profile) {
+    // Revoke associations if the selfManaged companion device does not connect for 3 months.
+    void removeInactiveSelfManagedAssociations() {
+        final long currentTime = System.currentTimeMillis();
+        long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1);
+        if (removalWindow <= 0) {
+            // 0 or negative values indicate that the sysprop was never set or should be ignored.
+            removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
+        }
+
         for (AssociationInfo ai : mAssociationStore.getAssociations()) {
-            if (ai.isSelfManaged()
-                    && profile.equals(ai.getDeviceProfile())
-                    && System.currentTimeMillis() - ai.getLastTimeConnectedMs()
-                    >= ASSOCIATION_CLEAN_UP_TIME_WINDOW) {
-                Slog.i(TAG, "Removing the association for associationId: "
-                        + ai.getId()
-                        + " due to the device does not connect for 3 months.");
+            if (!ai.isSelfManaged()) continue;
+            final boolean isInactive =  currentTime - ai.getLastTimeConnectedMs() >= removalWindow;
+            if (isInactive) {
+                Slog.i(TAG, "Removing inactive self-managed association: " + ai.getId());
                 disassociateInternal(ai.getId());
             }
         }
@@ -1078,10 +1084,10 @@
         return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
     }
 
-    private class LocalService extends CompanionDeviceManagerServiceInternal {
+    private class LocalService implements CompanionDeviceManagerServiceInternal {
         @Override
-        public void associationCleanUp(String profile) {
-            CompanionDeviceManagerService.this.associationCleanUp(profile);
+        public void removeInactiveSelfManagedAssociations() {
+            CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations();
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
index 326fefe..3649240 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
@@ -18,12 +18,10 @@
 
 /**
  * Companion Device Manager Local System Service Interface.
- *
- * @hide Only for use within the system server.
  */
-public abstract class CompanionDeviceManagerServiceInternal {
+interface CompanionDeviceManagerServiceInternal {
     /**
-     * @see CompanionDeviceManagerService#associationCleanUp
+     * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations
      */
-    public abstract void associationCleanUp(String profile);
+    void removeInactiveSelfManagedAssociations();
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 6a19a42..434d283 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -17,6 +17,7 @@
 package com.android.server.companion;
 
 import android.companion.AssociationInfo;
+import android.os.Binder;
 import android.os.ShellCommand;
 import android.util.Log;
 import android.util.Slog;
@@ -96,6 +97,16 @@
                     mDevicePresenceMonitor.simulateDeviceDisappeared(associationId);
                     break;
 
+                case "remove-inactive-associations": {
+                    // This command should trigger the same "clean-up" job as performed by the
+                    // InactiveAssociationsRemovalService JobService. However, since the
+                    // InactiveAssociationsRemovalService run as system, we want to run this
+                    // as system (not as shell/root) as well.
+                    Binder.withCleanCallingIdentity(
+                            mService::removeInactiveSelfManagedAssociations);
+                }
+                break;
+
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -142,6 +153,12 @@
         pw.println("      invoked for the same device (same ASSOCIATION_ID) no longer than");
         pw.println("      60 seconds ago.");
         pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
+        pw.println("  remove-inactive-associations");
+        pw.println("      Remove self-managed associations that have not been active ");
+        pw.println("      for a long time (90 days or as configured via ");
+        pw.println("      \"debug.cdm.cdmservice.cleanup_time_window\" system property). ");
+        pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
     }
 
     private int getNextIntArgRequired() {
diff --git a/services/companion/java/com/android/server/companion/AssociationCleanUpService.java b/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java
similarity index 68%
rename from services/companion/java/com/android/server/companion/AssociationCleanUpService.java
rename to services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java
index 55246e1..3482863 100644
--- a/services/companion/java/com/android/server/companion/AssociationCleanUpService.java
+++ b/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -24,10 +24,8 @@
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
 import android.app.job.JobService;
-import android.companion.AssociationRequest;
 import android.content.ComponentName;
 import android.content.Context;
-import android.os.AsyncTask;
 import android.util.Slog;
 
 import com.android.server.LocalServices;
@@ -37,26 +35,24 @@
  * The job will be executed only if the device is charging and in idle mode due to the application
  * will be killed if association/role are revoked.
  */
-public class AssociationCleanUpService extends JobService {
-    private static final int JOB_ID = AssociationCleanUpService.class.hashCode();
+public class InactiveAssociationsRemovalService extends JobService {
+    private static final int JOB_ID = InactiveAssociationsRemovalService.class.hashCode();
     private static final long ONE_DAY_INTERVAL = DAYS.toMillis(1);
 
     @Override
     public boolean onStartJob(final JobParameters params) {
-        Slog.i(TAG, "Execute the Association CleanUp job");
-        // Special policy for APP_STREAMING role that need to revoke associations if the device
-        // does not connect for 3 months.
-        AsyncTask.execute(() -> {
-            LocalServices.getService(CompanionDeviceManagerServiceInternal.class)
-                    .associationCleanUp(AssociationRequest.DEVICE_PROFILE_APP_STREAMING);
-            jobFinished(params, false);
-        });
+        Slog.i(TAG, "Execute the Association Removal job");
+        // Special policy for selfManaged that need to revoke associations if the device
+        // does not connect for 90 days.
+        LocalServices.getService(CompanionDeviceManagerServiceInternal.class)
+                .removeInactiveSelfManagedAssociations();
+        jobFinished(params, false);
         return true;
     }
 
     @Override
     public boolean onStopJob(final JobParameters params) {
-        Slog.i(TAG, "Association cleanup job stopped; id=" + params.getJobId()
+        Slog.i(TAG, "Association removal job stopped; id=" + params.getJobId()
                 + ", reason="
                 + JobParameters.getInternalReasonCodeDescription(
                 params.getInternalStopReasonCode()));
@@ -64,10 +60,10 @@
     }
 
     static void schedule(Context context) {
-        Slog.i(TAG, "Scheduling the Association Cleanup job");
+        Slog.i(TAG, "Scheduling the Association Removal job");
         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
         final JobInfo job = new JobInfo.Builder(JOB_ID,
-                new ComponentName(context, AssociationCleanUpService.class))
+                new ComponentName(context, InactiveAssociationsRemovalService.class))
                 .setRequiresCharging(true)
                 .setRequiresDeviceIdle(true)
                 .setPeriodic(ONE_DAY_INTERVAL)
@@ -75,3 +71,4 @@
         jobScheduler.schedule(job);
     }
 }
+
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 80182d2..dc7573e 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -37,6 +37,7 @@
 import android.util.Slog;
 import android.view.Display;
 import android.view.InputDevice;
+import android.view.WindowManager;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -83,22 +84,26 @@
     private final NativeWrapper mNativeWrapper;
     private final DisplayManagerInternal mDisplayManagerInternal;
     private final InputManagerInternal mInputManagerInternal;
+    private final WindowManager mWindowManager;
     private final DeviceCreationThreadVerifier mThreadVerifier;
 
-    InputController(@NonNull Object lock, @NonNull Handler handler) {
-        this(lock, new NativeWrapper(), handler,
+    InputController(@NonNull Object lock, @NonNull Handler handler,
+            @NonNull WindowManager windowManager) {
+        this(lock, new NativeWrapper(), handler, windowManager,
                 // Verify that virtual devices are not created on the handler thread.
                 () -> !handler.getLooper().isCurrentThread());
     }
 
     @VisibleForTesting
     InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper,
-            @NonNull Handler handler, @NonNull DeviceCreationThreadVerifier threadVerifier) {
+            @NonNull Handler handler, @NonNull WindowManager windowManager,
+            @NonNull DeviceCreationThreadVerifier threadVerifier) {
         mLock = lock;
         mHandler = handler;
         mNativeWrapper = nativeWrapper;
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
         mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
+        mWindowManager = windowManager;
         mThreadVerifier = threadVerifier;
     }
 
@@ -209,6 +214,15 @@
         mInputManagerInternal.setDisplayEligibilityForPointerCapture(displayId, isEligible);
     }
 
+    void setLocalIme(int displayId) {
+        // WM throws a SecurityException if the display is untrusted.
+        if ((mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
+                == Display.FLAG_TRUSTED) {
+            mWindowManager.setDisplayImePolicy(displayId,
+                    WindowManager.DISPLAY_IME_POLICY_LOCAL);
+        }
+    }
+
     @GuardedBy("mLock")
     private void updateActivePointerDisplayIdLocked() {
         InputDeviceDescriptor mostRecentlyCreatedMouse = null;
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 9802b97..638b3ae 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -62,6 +62,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
+import android.view.WindowManager;
 import android.widget.Toast;
 import android.window.DisplayWindowPolicyController;
 
@@ -167,7 +168,9 @@
         mParams = params;
         if (inputController == null) {
             mInputController = new InputController(
-                    mVirtualDeviceLock, context.getMainThreadHandler());
+                    mVirtualDeviceLock,
+                    context.getMainThreadHandler(),
+                    context.getSystemService(WindowManager.class));
         } else {
             mInputController = inputController;
         }
@@ -537,6 +540,7 @@
             mInputController.setPointerAcceleration(1f, displayId);
             mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
                     displayId);
+            mInputController.setLocalIme(displayId);
 
             // Since we're being called in the middle of the display being created, we post a
             // task to grab the wakelock instead of doing it synchronously here, to avoid
diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java
index c09bb2d..1a566a9 100644
--- a/services/core/java/com/android/server/am/AppBatteryTracker.java
+++ b/services/core/java/com/android/server/am/AppBatteryTracker.java
@@ -1258,6 +1258,16 @@
                 + "current_drain_event_duration_based_threshold_enabled";
 
         /**
+         * Whether or not we should move the app into the restricted bucket level if its background
+         * battery usage goes beyond the threshold. Note this different from the flag
+         * {@link AppRestrictionController.ConstantsObserver#KEY_BG_AUTO_RESTRICT_ABUSIVE_APPS}
+         * which is to control the overall auto bg restrictions.
+         */
+        static final String KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED =
+                DEVICE_CONFIG_SUBNAMESPACE_PREFIX
+                + "current_drain_auto_restrict_abusive_apps_enabled";
+
+        /**
          * The types of battery drain we're checking on each app; if the sum of the battery drain
          * exceeds the threshold, it'll be moved to restricted standby bucket; the type here
          * must be one of, or combination of {@link #BATTERY_USAGE_TYPE_BACKGROUND},
@@ -1353,6 +1363,11 @@
         final boolean mDefaultBgCurrentDrainEventDurationBasedThresholdEnabled;
 
         /**
+         * Default value to {@link #mBgCurrentDrainAutoRestrictAbusiveAppsEnabled}.
+         */
+        final boolean mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled;
+
+        /**
          * Default value to {@link #mBgCurrentDrainRestrictedBucketTypes}.
          */
         final int mDefaultCurrentDrainTypesToRestrictedBucket;
@@ -1430,6 +1445,11 @@
         volatile boolean mBgCurrentDrainEventDurationBasedThresholdEnabled;
 
         /**
+         * @see #KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED.
+         */
+        volatile boolean mBgCurrentDrainAutoRestrictAbusiveAppsEnabled;
+
+        /**
          * @see #KEY_BG_CURRENT_DRAIN_TYPES_TO_RESTRICTED_BUCKET.
          */
         volatile int mBgCurrentDrainRestrictedBucketTypes;
@@ -1520,6 +1540,8 @@
                     R.integer.config_bg_current_drain_location_min_duration) * 1_000;
             mDefaultBgCurrentDrainEventDurationBasedThresholdEnabled = resources.getBoolean(
                     R.bool.config_bg_current_drain_event_duration_based_threshold_enabled);
+            mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled = resources.getBoolean(
+                    R.bool.config_bg_current_drain_auto_restrict_abusive_apps);
             mDefaultCurrentDrainTypesToRestrictedBucket = resources.getInteger(
                     R.integer.config_bg_current_drain_types_to_restricted_bucket);
             mDefaultBgCurrentDrainTypesToBgRestricted = resources.getInteger(
@@ -1569,6 +1591,9 @@
                 case KEY_BG_CURRENT_DRAIN_POWER_COMPONENTS:
                     updateCurrentDrainThreshold();
                     break;
+                case KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED:
+                    updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled();
+                    break;
                 case KEY_BG_CURRENT_DRAIN_WINDOW:
                     updateCurrentDrainWindow();
                     break;
@@ -1701,6 +1726,13 @@
                     DEFAULT_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLD);
         }
 
+        private void updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled() {
+            mBgCurrentDrainAutoRestrictAbusiveAppsEnabled = DeviceConfig.getBoolean(
+                    DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                    KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED,
+                    mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled);
+        }
+
         @Override
         public void onSystemReady() {
             mBatteryFullChargeMah =
@@ -1714,6 +1746,7 @@
             updateCurrentDrainEventDurationBasedThresholdEnabled();
             updateCurrentDrainExemptedTypes();
             updateCurrentDrainDecoupleThresholds();
+            updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled();
         }
 
         @Override
@@ -1728,9 +1761,12 @@
                 if (pair != null) {
                     final long lastInteractionTime = mLastInteractionTime.get(uid, 0L);
                     final long[] ts = pair.first;
-                    final int restrictedLevel = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET]
-                            > (lastInteractionTime + mBgCurrentDrainInteractionGracePeriodMs)
-                            && mTracker.mAppRestrictionController.isAutoRestrictAbusiveAppEnabled()
+                    final boolean noInteractionRecently = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET]
+                            > (lastInteractionTime + mBgCurrentDrainInteractionGracePeriodMs);
+                    final boolean canRestrict =
+                            mTracker.mAppRestrictionController.isAutoRestrictAbusiveAppEnabled()
+                            && mBgCurrentDrainAutoRestrictAbusiveAppsEnabled;
+                    final int restrictedLevel = noInteractionRecently && canRestrict
                             ? RESTRICTION_LEVEL_RESTRICTED_BUCKET
                             : RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
                     if (maxLevel > RESTRICTION_LEVEL_BACKGROUND_RESTRICTED) {
@@ -2066,6 +2102,10 @@
                 pw.print('=');
                 pw.println(mBgCurrentDrainEventDurationBasedThresholdEnabled);
                 pw.print(prefix);
+                pw.print(KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED);
+                pw.print('=');
+                pw.println(mBgCurrentDrainAutoRestrictAbusiveAppsEnabled);
+                pw.print(prefix);
                 pw.print(KEY_BG_CURRENT_DRAIN_TYPES_TO_RESTRICTED_BUCKET);
                 pw.print('=');
                 pw.println(batteryUsageTypesToString(mBgCurrentDrainRestrictedBucketTypes));
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index b9da144..b56654f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -3258,7 +3258,7 @@
             return AppOpsManager.MODE_IGNORED;
         }
         synchronized (this) {
-            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass)) {
+            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, true)) {
                 return AppOpsManager.MODE_IGNORED;
             }
             code = AppOpsManager.opToSwitch(code);
@@ -3483,7 +3483,7 @@
 
             final int switchCode = AppOpsManager.opToSwitch(code);
             final UidState uidState = ops.uidState;
-            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass)) {
+            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, false)) {
                 attributedOp.rejected(uidState.state, flags);
                 scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
                         AppOpsManager.MODE_IGNORED);
@@ -3997,7 +3997,8 @@
             final Op op = getOpLocked(ops, code, uid, true);
             final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
             final UidState uidState = ops.uidState;
-            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass);
+            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass,
+                    false);
             final int switchCode = AppOpsManager.opToSwitch(code);
             // If there is a non-default per UID policy (we set UID op mode only if
             // non-default) it takes over, otherwise use the per package policy.
@@ -4834,7 +4835,7 @@
     }
 
     private boolean isOpRestrictedLocked(int uid, int code, String packageName,
-            String attributionTag, @Nullable RestrictionBypass appBypass) {
+            String attributionTag, @Nullable RestrictionBypass appBypass, boolean isCheckOp) {
         int restrictionSetCount = mOpGlobalRestrictions.size();
 
         for (int i = 0; i < restrictionSetCount; i++) {
@@ -4851,7 +4852,8 @@
             // For each client, check that the given op is not restricted, or that the given
             // package is exempt from the restriction.
             ClientUserRestrictionState restrictionState = mOpUserRestrictions.valueAt(i);
-            if (restrictionState.hasRestriction(code, packageName, attributionTag, userHandle)) {
+            if (restrictionState.hasRestriction(code, packageName, attributionTag, userHandle,
+                    isCheckOp)) {
                 RestrictionBypass opBypass = opAllowSystemBypassRestriction(code);
                 if (opBypass != null) {
                     // If we are the system, bypass user restrictions for certain codes
@@ -7224,7 +7226,7 @@
         }
 
         public boolean hasRestriction(int restriction, String packageName, String attributionTag,
-                int userId) {
+                int userId, boolean isCheckOp) {
             if (perUserRestrictions == null) {
                 return false;
             }
@@ -7243,6 +7245,9 @@
                 return true;
             }
 
+            if (isCheckOp) {
+                return !perUserExclusions.includes(packageName);
+            }
             return !perUserExclusions.contains(packageName, attributionTag);
         }
 
@@ -7409,7 +7414,8 @@
                 int numRestrictions = mOpUserRestrictions.size();
                 for (int i = 0; i < numRestrictions; i++) {
                     if (mOpUserRestrictions.valueAt(i)
-                            .hasRestriction(code, pkg, attributionTag, user.getIdentifier())) {
+                            .hasRestriction(code, pkg, attributionTag, user.getIdentifier(),
+                                    false)) {
                         number++;
                     }
                 }
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index 8035526..8de150a 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -948,19 +948,15 @@
         if (!isColorModeAvailable(colorMode)) {
             final int[] mappedColorModes = getContext().getResources().getIntArray(
                     R.array.config_mappedColorModes);
-            if (colorMode == COLOR_MODE_BOOSTED && mappedColorModes.length > COLOR_MODE_NATURAL
-                    && isColorModeAvailable(mappedColorModes[COLOR_MODE_NATURAL])) {
-                colorMode = COLOR_MODE_NATURAL;
-            } else if (colorMode == COLOR_MODE_SATURATED
-                    && mappedColorModes.length > COLOR_MODE_AUTOMATIC
-                    && isColorModeAvailable(mappedColorModes[COLOR_MODE_AUTOMATIC])) {
-                colorMode = COLOR_MODE_AUTOMATIC;
-            } else if (colorMode == COLOR_MODE_AUTOMATIC
-                    && mappedColorModes.length > COLOR_MODE_SATURATED
-                    && isColorModeAvailable(mappedColorModes[COLOR_MODE_SATURATED])) {
-                colorMode = COLOR_MODE_SATURATED;
+            if (colorMode != -1 && mappedColorModes.length > colorMode
+                    && isColorModeAvailable(mappedColorModes[colorMode])) {
+                colorMode = mappedColorModes[colorMode];
             } else {
-                colorMode = -1;
+                final int[] availableColorModes = getContext().getResources().getIntArray(
+                        R.array.config_availableColorModes);
+                if (availableColorModes.length > 0) {
+                    colorMode = availableColorModes[0];
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index cb38d52..0cdf7bf 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -166,9 +166,10 @@
 
             if (PackageManagerServiceUtils.isSystemApp(uninstalledPs)) {
                 UserInfo userInfo = mUserManagerInternal.getUserInfo(userId);
-                if (userInfo == null || !userInfo.isAdmin()) {
+                if (userInfo == null || (!userInfo.isAdmin() && !mUserManagerInternal.getUserInfo(
+                        mUserManagerInternal.getProfileParentId(userId)).isAdmin())) {
                     Slog.w(TAG, "Not removing package " + packageName
-                            + " as only admin user may downgrade system apps");
+                            + " as only admin user (or their profile) may downgrade system apps");
                     EventLog.writeEvent(0x534e4554, "170646036", -1, packageName);
                     return PackageManager.DELETE_FAILED_USER_RESTRICTED;
                 }
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java b/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java
index f2e2f4f..281e1bd 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java
@@ -26,6 +26,7 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.util.ArrayMap;
+import android.util.EventLog;
 import android.util.Slog;
 
 import com.android.internal.R;
@@ -36,6 +37,7 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * @hide
@@ -277,8 +279,28 @@
     }
 
     /**
-     * @return {@code true} if the package declares duplicate permissions with different
-     * protection levels.
+     * Determines if a duplicate permission is malformed .i.e. defines different protection level
+     * or group.
+     */
+    private static boolean isMalformedDuplicate(ParsedPermission p1, ParsedPermission p2) {
+        // Since a permission tree is also added as a permission with normal protection
+        // level, we need to skip if the parsedPermission is a permission tree.
+        if (p1 == null || p2 == null || p1.isTree() || p2.isTree()) {
+            return false;
+        }
+
+        if (p1.getProtectionLevel() != p2.getProtectionLevel()) {
+            return true;
+        }
+        if (!Objects.equals(p1.getGroup(), p2.getGroup())) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return {@code true} if the package declares malformed duplicate permissions.
      */
     public static boolean declareDuplicatePermission(@NonNull ParsingPackage pkg) {
         final List<ParsedPermission> permissions = pkg.getPermissions();
@@ -289,10 +311,10 @@
                 final ParsedPermission parsedPermission = permissions.get(i);
                 final String name = parsedPermission.getName();
                 final ParsedPermission perm = checkDuplicatePerm.get(name);
-                // Since a permission tree is also added as a permission with normal protection
-                // level, we need to skip if the parsedPermission is a permission tree.
-                if (perm != null && !(perm.isTree() || parsedPermission.isTree())
-                        && perm.getProtectionLevel() != parsedPermission.getProtectionLevel()) {
+                if (isMalformedDuplicate(parsedPermission, perm)) {
+                    // Fix for b/213323615
+                    EventLog.writeEvent(0x534e4554, "213323615",
+                            "The package " + pkg.getPackageName() + " seems malicious");
                     return true;
                 }
                 checkDuplicatePerm.put(name, parsedPermission);
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index 6ee9c66..06a54a4 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -968,7 +968,7 @@
         if (ParsedPermissionUtils.declareDuplicatePermission(pkg)) {
             return input.error(
                     INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
-                    "Declare duplicate permissions with different protection levels."
+                    "Found duplicate permission with a different attribute value."
             );
         }
 
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 5c8cfff..9e0d7b5 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -46,6 +46,7 @@
 import android.app.ActivityTaskManager;
 import android.app.StatusBarManager;
 import android.app.WindowConfiguration;
+import android.content.res.Resources;
 import android.graphics.Rect;
 import android.util.ArrayMap;
 import android.util.IntArray;
@@ -123,14 +124,17 @@
      * Let remote insets controller control system bars regardless of other settings.
      */
     private boolean mRemoteInsetsControllerControlsSystemBars;
+    private final boolean mHideNavBarForKeyboard;
     private final float[] mTmpFloat9 = new float[9];
 
     InsetsPolicy(InsetsStateController stateController, DisplayContent displayContent) {
         mStateController = stateController;
         mDisplayContent = displayContent;
         mPolicy = displayContent.getDisplayPolicy();
-        mRemoteInsetsControllerControlsSystemBars = mPolicy.getContext().getResources().getBoolean(
+        final Resources r = mPolicy.getContext().getResources();
+        mRemoteInsetsControllerControlsSystemBars = r.getBoolean(
                 R.bool.config_remoteInsetsControllerControlsSystemBars);
+        mHideNavBarForKeyboard = r.getBoolean(R.bool.config_hideNavBarForKeyboard);
     }
 
     boolean getRemoteInsetsControllerControlsSystemBars() {
@@ -428,13 +432,15 @@
     private InsetsState adjustVisibilityForIme(WindowState w, InsetsState originalState,
             boolean copyState) {
         if (w.mIsImWindow) {
-            // Navigation bar insets is always visible to IME.
+            // If navigation bar is not hidden by IME, IME should always receive visible
+            // navigation bar insets.
+            final boolean navVisible = !mHideNavBarForKeyboard;
             final InsetsSource originalNavSource = originalState.peekSource(ITYPE_NAVIGATION_BAR);
-            if (originalNavSource != null && !originalNavSource.isVisible()) {
+            if (originalNavSource != null && originalNavSource.isVisible() != navVisible) {
                 final InsetsState state = copyState ? new InsetsState(originalState)
                         : originalState;
                 final InsetsSource navSource = new InsetsSource(originalNavSource);
-                navSource.setVisible(true);
+                navSource.setVisible(navVisible);
                 state.addSource(navSource);
                 return state;
             }
@@ -573,8 +579,9 @@
     private @Nullable InsetsControlTarget getNavControlTarget(@Nullable WindowState focusedWin,
             boolean fake) {
         final WindowState imeWin = mDisplayContent.mInputMethodWindow;
-        if (imeWin != null && imeWin.isVisible()) {
-            // Force showing navigation bar while IME is visible.
+        if (imeWin != null && imeWin.isVisible() && !mHideNavBarForKeyboard) {
+            // Force showing navigation bar while IME is visible and if navigation bar is not
+            // configured to be hidden by the IME.
             return null;
         }
         if (!fake && isShowingTransientTypes(Type.navigationBars())) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
index 009dae5..fa8d569 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
@@ -585,6 +585,7 @@
         DeviceConfigSession<Long> bgCurrentDrainInteractionGracePeriod = null;
         DeviceConfigSession<Float> bgCurrentDrainRestrictedBucketThreshold = null;
         DeviceConfigSession<Float> bgCurrentDrainBgRestrictedThreshold = null;
+        DeviceConfigSession<Boolean> bgCurrentDrainAutoRestrictAbusiveApps = null;
         DeviceConfigSession<Boolean> bgPromptFgsWithNotiToBgRestricted = null;
         DeviceConfigSession<Boolean> bgPromptAbusiveAppToBgRestricted = null;
         DeviceConfigSession<Long> bgNotificationMinInterval = null;
@@ -644,6 +645,14 @@
                             isLowRamDeviceStatic() ? 1 : 0]);
             bgCurrentDrainBgRestrictedThreshold.set(bgRestrictedThreshold);
 
+            bgCurrentDrainAutoRestrictAbusiveApps = new DeviceConfigSession<>(
+                    DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                    AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED,
+                    DeviceConfig::getBoolean,
+                    mContext.getResources().getBoolean(
+                            R.bool.config_bg_current_drain_auto_restrict_abusive_apps));
+            bgCurrentDrainAutoRestrictAbusiveApps.set(true);
+
             bgPromptFgsWithNotiToBgRestricted = new DeviceConfigSession<>(
                     DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                     ConstantsObserver.KEY_BG_PROMPT_FGS_WITH_NOTIFICATION_TO_BG_RESTRICTED,
@@ -1099,6 +1108,7 @@
             closeIfNotNull(bgCurrentDrainInteractionGracePeriod);
             closeIfNotNull(bgCurrentDrainRestrictedBucketThreshold);
             closeIfNotNull(bgCurrentDrainBgRestrictedThreshold);
+            closeIfNotNull(bgCurrentDrainAutoRestrictAbusiveApps);
             closeIfNotNull(bgPromptFgsWithNotiToBgRestricted);
             closeIfNotNull(bgPromptAbusiveAppToBgRestricted);
             closeIfNotNull(bgNotificationMinInterval);
@@ -1651,6 +1661,7 @@
         DeviceConfigSession<Float> bgCurrentDrainBgRestrictedThreshold = null;
         DeviceConfigSession<Float> bgCurrentDrainRestrictedBucketHighThreshold = null;
         DeviceConfigSession<Float> bgCurrentDrainBgRestrictedHighThreshold = null;
+        DeviceConfigSession<Boolean> bgCurrentDrainAutoRestrictAbusiveApps = null;
         DeviceConfigSession<Long> bgMediaPlaybackMinDurationThreshold = null;
         DeviceConfigSession<Long> bgLocationMinDurationThreshold = null;
         DeviceConfigSession<Boolean> bgCurrentDrainEventDurationBasedThresholdEnabled = null;
@@ -1736,6 +1747,14 @@
                             isLowRamDeviceStatic() ? 1 : 0]);
             bgCurrentDrainBgRestrictedHighThreshold.set(bgRestrictedHighThreshold);
 
+            bgCurrentDrainAutoRestrictAbusiveApps = new DeviceConfigSession<>(
+                    DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                    AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED,
+                    DeviceConfig::getBoolean,
+                    mContext.getResources().getBoolean(
+                            R.bool.config_bg_current_drain_auto_restrict_abusive_apps));
+            bgCurrentDrainAutoRestrictAbusiveApps.set(true);
+
             bgMediaPlaybackMinDurationThreshold = new DeviceConfigSession<>(
                     DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                     AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_MEDIA_PLAYBACK_MIN_DURATION,
@@ -2226,6 +2245,7 @@
             closeIfNotNull(bgCurrentDrainBgRestrictedThreshold);
             closeIfNotNull(bgCurrentDrainRestrictedBucketHighThreshold);
             closeIfNotNull(bgCurrentDrainBgRestrictedHighThreshold);
+            closeIfNotNull(bgCurrentDrainAutoRestrictAbusiveApps);
             closeIfNotNull(bgMediaPlaybackMinDurationThreshold);
             closeIfNotNull(bgLocationMinDurationThreshold);
             closeIfNotNull(bgCurrentDrainEventDurationBasedThresholdEnabled);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
index 8a954ca..5f9f1b2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
@@ -233,27 +233,34 @@
     @Test
     public void testConstantsUpdating_ValidValues() {
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 5 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 5 * MINUTE_IN_MILLIS);
 
         assertEquals(5 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+        assertEquals(5 * MINUTE_IN_MILLIS, mPrefetchController.getLaunchTimeAllowanceMs());
     }
 
     @Test
     public void testConstantsUpdating_InvalidValues() {
         // Test negatives/too low.
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 4 * MINUTE_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, -MINUTE_IN_MILLIS);
 
         assertEquals(HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+        assertEquals(0, mPrefetchController.getLaunchTimeAllowanceMs());
 
         // Test larger than a day. Controller should cap at one day.
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 25 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 5 * HOUR_IN_MILLIS);
 
         assertEquals(24 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+        assertEquals(2 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeAllowanceMs());
     }
 
     @Test
     public void testConstantsUpdating_ThresholdChangesAlarms() {
         final long launchDelayMs = 11 * HOUR_IN_MILLIS;
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
         when(mUsageStatsManagerInternal
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
                 .thenReturn(sSystemClock.millis() + launchDelayMs);
@@ -276,6 +283,7 @@
 
     @Test
     public void testConstraintNotSatisfiedWhenLaunchLate() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
 
         final JobStatus job = createJobStatus("testConstraintNotSatisfiedWhenLaunchLate", 1);
@@ -290,6 +298,8 @@
 
     @Test
     public void testConstraintSatisfiedWhenLaunchSoon() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
+
         final JobStatus job = createJobStatus("testConstraintSatisfiedWhenLaunchSoon", 2);
         when(mUsageStatsManagerInternal
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
@@ -338,6 +348,8 @@
 
     @Test
     public void testConstraintSatisfiedWhenWidget() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
+
         final JobStatus jobNonWidget = createJobStatus("testConstraintSatisfiedWhenWidget", 1);
         final JobStatus jobWidget = createJobStatus("testConstraintSatisfiedWhenWidget", 2);
 
@@ -365,6 +377,7 @@
     @Test
     public void testEstimatedLaunchTimeChangedToLate() {
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
         when(mUsageStatsManagerInternal
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
                 .thenReturn(sSystemClock.millis() + HOUR_IN_MILLIS);
@@ -393,6 +406,7 @@
     @Test
     public void testEstimatedLaunchTimeChangedToSoon() {
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
         when(mUsageStatsManagerInternal
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
                 .thenReturn(sSystemClock.millis() + 10 * HOUR_IN_MILLIS);
@@ -413,4 +427,36 @@
         verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any());
         assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
     }
+
+    @Test
+    public void testEstimatedLaunchTimeAllowance() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 15 * MINUTE_IN_MILLIS);
+        when(mUsageStatsManagerInternal
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
+                .thenReturn(sSystemClock.millis() + 10 * HOUR_IN_MILLIS);
+
+        InOrder inOrder = inOrder(mUsageStatsManagerInternal);
+
+        JobStatus jobStatus = createJobStatus("testEstimatedLaunchTimeAllowance", 1);
+        trackJobs(jobStatus);
+        inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        // The allowance shouldn't shift the alarm
+        verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1))
+                .setWindow(
+                        anyInt(), eq(sElapsedRealtimeClock.millis() + 3 * HOUR_IN_MILLIS),
+                        anyLong(), eq(TAG_PREFETCH), any(), any());
+        assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+
+        mEstimatedLaunchTimeChangedListener.onEstimatedLaunchTimeChanged(SOURCE_USER_ID,
+                SOURCE_PACKAGE, sSystemClock.millis() + HOUR_IN_MILLIS);
+
+        inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS).times(0))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any());
+        assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+
+        sSystemClock = getShiftedClock(sSystemClock, HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
index c9598bd..e30f3d2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
@@ -69,13 +69,34 @@
     }
 
     @Test
-    fun deleteSystemPackageFailsIfNotAdmin() {
+    fun deleteSystemPackageFailsIfNotAdminAndNotProfile() {
         val ps = mPms.mSettings.getPackageLPr("a.data.package")
         whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true)
         whenever(mUserManagerInternal.getUserInfo(1)).thenReturn(UserInfo(1, "test", 0))
+        whenever(mUserManagerInternal.getProfileParentId(1)).thenReturn(1)
 
         val dph = DeletePackageHelper(mPms)
-        val result = dph.deletePackageX("a.data.package", 1L, 1, 0, false)
+        val result = dph.deletePackageX("a.data.package", 1L, 1,
+            PackageManager.DELETE_SYSTEM_APP, false)
+
+        assertThat(result).isEqualTo(PackageManager.DELETE_FAILED_USER_RESTRICTED)
+    }
+
+    @Test
+    fun deleteSystemPackageFailsIfProfileOfNonAdmin() {
+        val userId = 1
+        val parentId = 5
+        val ps = mPms.mSettings.getPackageLPr("a.data.package")
+        whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true)
+        whenever(mUserManagerInternal.getUserInfo(userId)).thenReturn(
+            UserInfo(userId, "test", UserInfo.FLAG_PROFILE))
+        whenever(mUserManagerInternal.getProfileParentId(userId)).thenReturn(parentId)
+        whenever(mUserManagerInternal.getUserInfo(parentId)).thenReturn(
+            UserInfo(userId, "testparent", 0))
+
+        val dph = DeletePackageHelper(mPms)
+        val result = dph.deletePackageX("a.data.package", 1L, userId,
+            PackageManager.DELETE_SYSTEM_APP, false)
 
         assertThat(result).isEqualTo(PackageManager.DELETE_FAILED_USER_RESTRICTED)
     }
@@ -93,4 +114,23 @@
 
         assertThat(result).isEqualTo(PackageManager.DELETE_SUCCEEDED)
     }
+
+    @Test
+    fun deleteSystemPackageSucceedsIfProfileOfAdmin() {
+        val userId = 1
+        val parentId = 5
+        val ps = mPms.mSettings.getPackageLPr("a.data.package")
+        whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true)
+        whenever(mUserManagerInternal.getUserInfo(userId)).thenReturn(
+            UserInfo(userId, "test", UserInfo.FLAG_PROFILE))
+        whenever(mUserManagerInternal.getProfileParentId(userId)).thenReturn(parentId)
+        whenever(mUserManagerInternal.getUserInfo(parentId)).thenReturn(
+            UserInfo(userId, "testparent", UserInfo.FLAG_ADMIN))
+
+        val dph = DeletePackageHelper(mPms)
+        val result = dph.deletePackageX("a.data.package", 1L, userId,
+            PackageManager.DELETE_SYSTEM_APP, false)
+
+        assertThat(result).isEqualTo(PackageManager.DELETE_SUCCEEDED)
+    }
 }
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index 77cbb3a..5d9d765 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -34,6 +34,9 @@
 import android.testing.TestableLooper;
 import android.view.Display;
 import android.view.DisplayInfo;
+import android.view.WindowManager;
+
+import androidx.test.InstrumentationRegistry;
 
 import com.android.server.LocalServices;
 
@@ -79,7 +82,9 @@
         // Allow virtual devices to be created on the looper thread for testing.
         final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
         mInputController = new InputController(new Object(), mNativeWrapperMock,
-                new Handler(TestableLooper.get(this).getLooper()), threadVerifier);
+                new Handler(TestableLooper.get(this).getLooper()),
+                InstrumentationRegistry.getTargetContext().getSystemService(WindowManager.class),
+                threadVerifier);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index cbb9fd7..f9671e5 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -77,6 +77,7 @@
 import android.util.ArraySet;
 import android.view.DisplayInfo;
 import android.view.KeyEvent;
+import android.view.WindowManager;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -208,7 +209,8 @@
         // Allow virtual devices to be created on the looper thread for testing.
         final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
         mInputController = new InputController(new Object(), mNativeWrapperMock,
-                new Handler(TestableLooper.get(this).getLooper()), threadVerifier);
+                new Handler(TestableLooper.get(this).getLooper()),
+                mContext.getSystemService(WindowManager.class), threadVerifier);
 
         mAssociationInfo = new AssociationInfo(1, 0, null,
                 MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0);
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index 980ea5c..432af3a 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -2895,7 +2895,19 @@
             if (key != null) {
                 final Object value = bundle.get(key);
                 final Object newValue = newBundle.get(key);
-                if (!Objects.equals(value, newValue)) {
+                if (!newBundle.containsKey(key)) {
+                    return false;
+                }
+                if (value instanceof Bundle && newValue instanceof Bundle) {
+                    if (!areBundlesEqual((Bundle) value, (Bundle) newValue)) {
+                        return false;
+                    }
+                }
+                if (value instanceof byte[] && newValue instanceof byte[]) {
+                    if (!Arrays.equals((byte[]) value, (byte[]) newValue)) {
+                        return false;
+                    }
+                } else if (!Objects.equals(value, newValue)) {
                     return false;
                 }
             }
diff --git a/telephony/common/android/telephony/LocationAccessPolicy.java b/telephony/common/android/telephony/LocationAccessPolicy.java
index 9dfb0cc..d4b6c91 100644
--- a/telephony/common/android/telephony/LocationAccessPolicy.java
+++ b/telephony/common/android/telephony/LocationAccessPolicy.java
@@ -316,9 +316,11 @@
             return LocationPermissionResult.ALLOWED;
         }
 
-        // Check the system-wide requirements. If the location main switch is off or
-        // the app's profile isn't in foreground, return a soft denial.
-        if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid)) {
+        // Check the system-wide requirements. If the location main switch is off and the caller is
+        // not in the allowlist of apps that always have loation access or the app's profile
+        // isn't in the foreground, return a soft denial.
+        if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid,
+                query.callingPackage)) {
             return LocationPermissionResult.DENIED_SOFT;
         }
 
@@ -344,15 +346,16 @@
         return LocationPermissionResult.ALLOWED;
     }
 
-
     private static boolean checkManifestPermission(Context context, int pid, int uid,
             String permissionToCheck) {
         return context.checkPermission(permissionToCheck, pid, uid)
                 == PackageManager.PERMISSION_GRANTED;
     }
 
-    private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid) {
-        if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier())) {
+    private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid,
+            @NonNull String callingPackage) {
+        if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier())
+                && !isLocationBypassAllowed(context, callingPackage)) {
             if (DBG) Log.w(TAG, "Location disabled, failed, (" + uid + ")");
             return false;
         }
@@ -373,6 +376,16 @@
         return locationManager.isLocationEnabledForUser(UserHandle.of(userId));
     }
 
+    private static boolean isLocationBypassAllowed(@NonNull Context context,
+            @NonNull String callingPackage) {
+        for (String bypassPackage : getLocationBypassPackages(context)) {
+            if (callingPackage.equals(bypassPackage)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * @return An array of packages that are always allowed to access location.
      */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index a6e3bb4..b6f8652 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17006,4 +17006,41 @@
         }
         mTelephonyRegistryMgr.removeCarrierPrivilegesCallback(callback);
     }
+
+    /**
+     * set removable eSIM as default eUICC.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_EUICC)
+    public void setRemovableEsimAsDefaultEuicc(boolean isDefault) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                telephony.setRemovableEsimAsDefaultEuicc(isDefault, getOpPackageName());
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in setRemovableEsimAsDefault: " + e);
+        }
+    }
+
+    /**
+     * Returns whether the removable eSIM is default eUICC or not.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_EUICC)
+    public boolean isRemovableEsimDefaultEuicc() {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.isRemovableEsimDefaultEuicc(getOpPackageName());
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in isRemovableEsimDefaultEuicc: " + e);
+        }
+        return false;
+    }
 }
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index eede9dc..a673807 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -25,18 +25,17 @@
 import android.annotation.SystemApi;
 import android.app.Activity;
 import android.app.PendingIntent;
-import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
 import android.content.pm.PackageManager;
-import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyFrameworkInitializer;
 import android.telephony.TelephonyManager;
 import android.telephony.UiccCardInfo;
@@ -1209,18 +1208,16 @@
             return;
         }
         try {
-            // TODO: Uncomment below compat change code once callers are ported to use
-            //  switchToSubscription with portIndex for disable operation.
-            // if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
-            //        && getIEuiccController().isCompatChangeEnabled(mContext.getOpPackageName(),
-            //        SWITCH_WITHOUT_PORT_INDEX_EXCEPTION_ON_DISABLE)) {
-            //    // Apps targeting on Android T and beyond will get exception whenever
-            //    // switchToSubscription without portIndex is called with INVALID_SUBSCRIPTION_ID.
-            //    Log.e(TAG, "switchToSubscription without portIndex is not allowed for"
-            //            + " disable operation");
-            //    throw new IllegalArgumentException("Must use switchToSubscription with portIndex"
-            //            + " API for disable operation");
-            // }
+            if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                     && getIEuiccController().isCompatChangeEnabled(mContext.getOpPackageName(),
+                     SWITCH_WITHOUT_PORT_INDEX_EXCEPTION_ON_DISABLE)) {
+                // Apps targeting on Android T and beyond will get exception whenever
+                // switchToSubscription without portIndex is called with INVALID_SUBSCRIPTION_ID.
+                Log.e(TAG, "switchToSubscription without portIndex is not allowed for"
+                        + " disable operation");
+                throw new IllegalArgumentException("Must use switchToSubscription with portIndex"
+                        + " API for disable operation");
+            }
             getIEuiccController().switchToSubscription(mCardId,
                     subscriptionId, mContext.getOpPackageName(), callbackIntent);
         } catch (RemoteException e) {
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 7d116f9..0ce6b14 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2553,4 +2553,18 @@
      * for the slot, or {@code null} if none is resolved
      */
     String getCarrierServicePackageNameForLogicalSlot(int logicalSlotIndex);
+
+    /**
+     * set removable eSIM as default eUICC.
+     *
+     * @hide
+     */
+    void setRemovableEsimAsDefaultEuicc(boolean isDefault, String callingPackage);
+
+    /**
+     * Returns whether the removable eSIM is default eUICC or not.
+     *
+     * @hide
+     */
+    boolean isRemovableEsimDefaultEuicc(String callingPackage);
 }