Merge "Sending TaskFragmentInfo and operation type in #onTaskFragmentError" into tm-qpr-dev
diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java
index 1769993e..f97415c 100644
--- a/core/java/android/app/NotificationChannelGroup.java
+++ b/core/java/android/app/NotificationChannelGroup.java
@@ -20,7 +20,6 @@
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Intent;
-import android.content.pm.ParceledListSlice;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
@@ -67,7 +66,7 @@
     private CharSequence mName;
     private String mDescription;
     private boolean mBlocked;
-    private ParceledListSlice<NotificationChannel> mChannels;
+    private List<NotificationChannel> mChannels = new ArrayList<>();
     // Bitwise representation of fields that have been changed by the user
     private int mUserLockedFields;
 
@@ -101,8 +100,7 @@
         } else {
             mDescription = null;
         }
-        mChannels = in.readParcelable(
-                NotificationChannelGroup.class.getClassLoader(), ParceledListSlice.class);
+        in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader(), android.app.NotificationChannel.class);
         mBlocked = in.readBoolean();
         mUserLockedFields = in.readInt();
     }
@@ -129,7 +127,7 @@
         } else {
             dest.writeByte((byte) 0);
         }
-        dest.writeParcelable(mChannels, flags);
+        dest.writeParcelableList(mChannels, flags);
         dest.writeBoolean(mBlocked);
         dest.writeInt(mUserLockedFields);
     }
@@ -159,7 +157,7 @@
      * Returns the list of channels that belong to this group
      */
     public List<NotificationChannel> getChannels() {
-        return mChannels == null ? new ArrayList<>() : mChannels.getList();
+        return mChannels;
     }
 
     /**
@@ -193,8 +191,15 @@
     /**
      * @hide
      */
+    public void addChannel(NotificationChannel channel) {
+        mChannels.add(channel);
+    }
+
+    /**
+     * @hide
+     */
     public void setChannels(List<NotificationChannel> channels) {
-        mChannels = new ParceledListSlice<>(channels);
+        mChannels = channels;
     }
 
     /**
@@ -329,7 +334,7 @@
         proto.write(NotificationChannelGroupProto.NAME, mName.toString());
         proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription);
         proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked);
-        for (NotificationChannel channel : mChannels.getList()) {
+        for (NotificationChannel channel : mChannels) {
             channel.dumpDebug(proto, NotificationChannelGroupProto.CHANNELS);
         }
         proto.end(token);
diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java
index 99e4feb..943eee4 100644
--- a/core/java/android/hardware/biometrics/BiometricConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricConstants.java
@@ -157,6 +157,11 @@
     int BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED = 18;
 
     /**
+     * A power press stopped this biometric operation.
+     * @hide
+     */
+    int BIOMETRIC_ERROR_POWER_PRESSED = 19;
+    /**
      * This constant is only used by SystemUI. It notifies SystemUI that authentication was paused
      * because the authentication attempt was unsuccessful.
      * @hide
diff --git a/core/java/android/hardware/biometrics/BiometricFaceConstants.java b/core/java/android/hardware/biometrics/BiometricFaceConstants.java
index fd46f24..2b62b98 100644
--- a/core/java/android/hardware/biometrics/BiometricFaceConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricFaceConstants.java
@@ -70,6 +70,7 @@
             BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED,
             BIOMETRIC_ERROR_RE_ENROLL,
             FACE_ERROR_UNKNOWN,
+            BIOMETRIC_ERROR_POWER_PRESSED,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface FaceError {}
@@ -184,6 +185,12 @@
     int FACE_ERROR_UNKNOWN = 17;
 
     /**
+     * A power press stopped this biometric operation.
+     * @hide
+     */
+    int BIOMETRIC_ERROR_POWER_PRESSED = 19;
+
+    /**
      * Vendor codes received from the HAL start at 0. Codes that the framework exposes to keyguard
      * append this value for some reason. We should probably remove this and just send the actual
      * vendor code.
diff --git a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java
index d8ebb62..98f571b 100644
--- a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java
@@ -61,7 +61,8 @@
             BIOMETRIC_ERROR_RE_ENROLL,
             BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED,
             FINGERPRINT_ERROR_UNKNOWN,
-            FINGERPRINT_ERROR_BAD_CALIBRATION})
+            FINGERPRINT_ERROR_BAD_CALIBRATION,
+            BIOMETRIC_ERROR_POWER_PRESSED})
     @Retention(RetentionPolicy.SOURCE)
     @interface FingerprintError {}
 
@@ -188,6 +189,12 @@
     int FINGERPRINT_ERROR_BAD_CALIBRATION = 18;
 
     /**
+     * A power press stopped this biometric operation.
+     * @hide
+     */
+    int BIOMETRIC_ERROR_POWER_PRESSED = 19;
+
+    /**
      * @hide
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
diff --git a/core/java/android/hardware/biometrics/BiometricStateListener.java b/core/java/android/hardware/biometrics/BiometricStateListener.java
index 2ac0c1e..b167cc6 100644
--- a/core/java/android/hardware/biometrics/BiometricStateListener.java
+++ b/core/java/android/hardware/biometrics/BiometricStateListener.java
@@ -46,6 +46,14 @@
     public @interface State {
     }
 
+    // The sensor received a touch.
+    public static final int ACTION_SENSOR_TOUCH = 0;
+
+    @IntDef({ACTION_SENSOR_TOUCH})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {
+    }
+
     /**
      * Defines behavior in response to state update
      * @param newState new state of the biometric sensor
@@ -53,6 +61,13 @@
     public void onStateChanged(@BiometricStateListener.State int newState) {
     }
 
+
+    /**
+     * Invoked when a biometric action has occurred.
+     */
+    public void onBiometricAction(@BiometricStateListener.Action int action) {
+    }
+
     /**
      * Invoked when enrollment state changes for the specified user
      */
diff --git a/core/java/android/hardware/biometrics/IBiometricStateListener.aidl b/core/java/android/hardware/biometrics/IBiometricStateListener.aidl
index 5bdced0..6bb170d 100644
--- a/core/java/android/hardware/biometrics/IBiometricStateListener.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricStateListener.aidl
@@ -22,5 +22,6 @@
  */
 oneway interface IBiometricStateListener {
     void onStateChanged(int newState);
+    void onBiometricAction(int action);
     void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments);
 }
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 8db298f..861a850 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -1325,8 +1325,11 @@
      * flashlight brightness level via
      * {@link android.hardware.camera2.CameraManager#turnOnTorchWithStrengthLevel }.
      * If this value is equal to 1, flashlight brightness control is not supported.
-     * The value for this key will be null for devices with no flash unit.
-     * This level must be set to a safe value to prevent any burn out issues.</p>
+     * The value for this key will be null for devices with no flash unit.</p>
+     * <p>The maximum value is guaranteed to be safe to use for an indefinite duration in
+     * terms of device flashlight lifespan, but may be too bright for comfort for many
+     * use cases. Use the default torch brightness value to avoid problems with an
+     * over-bright flashlight.</p>
      * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
      */
     @PublicKey
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 28f1f02..c614cdb 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -101,6 +101,7 @@
     private static final int MSG_FINGERPRINT_DETECTED = 107;
     private static final int MSG_UDFPS_POINTER_DOWN = 108;
     private static final int MSG_UDFPS_POINTER_UP = 109;
+    private static final int MSG_POWER_BUTTON_PRESSED = 110;
 
     /**
      * @hide
@@ -984,6 +985,16 @@
     }
 
     /**
+     * This is triggered by SideFpsEventHandler
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void onPowerPressed() {
+        Slog.i(TAG, "onPowerPressed");
+        mHandler.obtainMessage(MSG_POWER_BUTTON_PRESSED).sendToTarget();
+    }
+
+    /**
      * Determine if there is at least one fingerprint enrolled.
      *
      * @return true if at least one fingerprint is enrolled, false otherwise
@@ -1196,6 +1207,9 @@
                 case MSG_UDFPS_POINTER_UP:
                     sendUdfpsPointerUp(msg.arg1 /* sensorId */);
                     break;
+                case MSG_POWER_BUTTON_PRESSED:
+                    sendPowerPressed();
+                    break;
                 default:
                     Slog.w(TAG, "Unknown message: " + msg.what);
 
@@ -1325,6 +1339,14 @@
         mAuthenticationCallback.onUdfpsPointerUp(sensorId);
     }
 
+    private void sendPowerPressed() {
+        try {
+            mService.onPowerPressed();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error sending power press", e);
+        }
+    }
+
     /**
      * @hide
      */
@@ -1461,6 +1483,9 @@
             case FINGERPRINT_ERROR_BAD_CALIBRATION:
                 return context.getString(
                             com.android.internal.R.string.fingerprint_error_bad_calibration);
+            case BIOMETRIC_ERROR_POWER_PRESSED:
+                return context.getString(
+                    com.android.internal.R.string.fingerprint_error_power_pressed);
             case FINGERPRINT_ERROR_VENDOR: {
                 String[] msgArray = context.getResources().getStringArray(
                         com.android.internal.R.array.fingerprint_error_vendor);
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index 0b63446..20cc58c 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -171,4 +171,7 @@
 
     // Registers BiometricStateListener.
     void registerBiometricStateListener(IBiometricStateListener listener);
+
+    // Sends a power button pressed event to all listeners.
+    oneway void onPowerPressed();
 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 7964f7c..ce35461 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9704,6 +9704,43 @@
         public static final String FACE_UNLOCK_RE_ENROLL = "face_unlock_re_enroll";
 
         /**
+         * The time (in millis) to wait for a power button before sending a
+         * successful auth in to keyguard(for side fingerprint)
+         * @hide
+         */
+        @Readable
+        public static final String FINGERPRINT_SIDE_FPS_KG_POWER_WINDOW =
+                "fingerprint_side_fps_kg_power_window";
+
+        /**
+         * The time (in millis) to wait for a power button before sending
+         * a successful auth in biometric prompt(for side fingerprint)
+         * @hide
+         */
+        @Readable
+        public static final String FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW =
+                "fingerprint_side_fps_bp_power_window";
+
+        /**
+         * The time (in millis) that a finger tap will wait for a power button
+         * before dismissing the power dialog during enrollment(for side
+         * fingerprint)
+         * @hide
+         */
+        @Readable
+        public static final String FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW =
+                "fingerprint_side_fps_enroll_tap_window";
+
+        /**
+         * The time (in millis) that a power event will ignore future authentications
+         * (for side fingerprint)
+         * @hide
+         */
+        @Readable
+        public static final String FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME =
+                "fingerprint_side_fps_auth_downtime";
+
+        /**
          * Whether or not debugging is enabled.
          * @hide
          */
@@ -10880,6 +10917,14 @@
         public static final String ADAPTIVE_CHARGING_ENABLED = "adaptive_charging_enabled";
 
         /**
+         * Whether battery saver is currently set to different schedule mode.
+         *
+         * @hide
+         */
+        public static final String EXTRA_AUTOMATIC_POWER_SAVE_MODE =
+                "extra_automatic_power_save_mode";
+
+        /**
          * These entries are considered common between the personal and the managed profile,
          * since the managed profile doesn't get to change them.
          */
@@ -11301,6 +11346,7 @@
          * <li>{@link BatteryManager#BATTERY_PLUGGED_AC} to stay on for AC charger</li>
          * <li>{@link BatteryManager#BATTERY_PLUGGED_USB} to stay on for USB charger</li>
          * <li>{@link BatteryManager#BATTERY_PLUGGED_WIRELESS} to stay on for wireless charger</li>
+         * <li>{@link BatteryManager#BATTERY_PLUGGED_DOCK} to stay on for dock charger</li>
          * </ul>
          * These values can be OR-ed together.
          */
diff --git a/core/java/android/util/SafetyProtectionUtils.java b/core/java/android/util/SafetyProtectionUtils.java
new file mode 100644
index 0000000..af985c5
--- /dev/null
+++ b/core/java/android/util/SafetyProtectionUtils.java
@@ -0,0 +1,53 @@
+/*
+ * 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 android.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.DeviceConfig;
+
+/**
+ * Util class for whether we should show the safety protection resources.
+ *
+ * @hide
+ */
+public class SafetyProtectionUtils {
+    private static final String SAFETY_PROTECTION_RESOURCES_ENABLED = "safety_protection_enabled";
+
+    /**
+     * Determines whether we should show the safety protection resources.
+     * We show the resources only if
+     * (1) the feature flag safety_protection_enabled is enabled and
+     * (2) the config value config_safetyProtectionEnabled is enabled/true and
+     * (3) the resources exist (currently the resources only exist on GMS devices)
+     *
+     * TODO: make this an API in U
+     *
+     * @hide
+     */
+    public static boolean shouldShowSafetyProtectionResources(Context context) {
+        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+                SAFETY_PROTECTION_RESOURCES_ENABLED, false)
+                && context.getResources().getBoolean(
+                        Resources.getSystem()
+                                .getIdentifier("config_safetyProtectionEnabled",
+                                        "bool", "android"))
+                && context.getDrawable(android.R.drawable.ic_safety_protection) != null
+                && context.getString(android.R.string.safety_protection_display_text) != null
+                && !context.getString(android.R.string.safety_protection_display_text).isEmpty();
+    }
+}
diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java
index dbf3570..ca57c84 100644
--- a/core/java/android/widget/Toast.java
+++ b/core/java/android/widget/Toast.java
@@ -32,6 +32,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
@@ -494,19 +495,39 @@
      */
     public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
             @NonNull CharSequence text, @Duration int duration) {
-        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
-            Toast result = new Toast(context, looper);
-            result.mText = text;
-            result.mDuration = duration;
-            return result;
-        } else {
-            Toast result = new Toast(context, looper);
-            View v = ToastPresenter.getTextToastView(context, text);
-            result.mNextView = v;
-            result.mDuration = duration;
+        Toast result = new Toast(context, looper);
 
-            return result;
+        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
+            result.mText = text;
+        } else {
+            result.mNextView = ToastPresenter.getTextToastView(context, text);
         }
+
+        result.mDuration = duration;
+        return result;
+    }
+
+    /**
+     * Make a standard toast with an icon to display using the specified looper.
+     * If looper is null, Looper.myLooper() is used.
+     *
+     * The toast will be a custom view that's rendered by the app (instead of by SystemUI).
+     * In Android version R and above, non-system apps can only render the toast
+     * when it's in the foreground.
+     *
+     * @hide
+     */
+    public static Toast makeCustomToastWithIcon(@NonNull Context context, @Nullable Looper looper,
+            @NonNull CharSequence text, @Duration int duration, @NonNull Drawable icon) {
+        if (icon == null) {
+            throw new IllegalArgumentException("Drawable icon should not be null "
+                    + "for makeCustomToastWithIcon");
+        }
+
+        Toast result = new Toast(context, looper);
+        result.mNextView = ToastPresenter.getTextToastViewWithIcon(context, text, icon);
+        result.mDuration = duration;
+        return result;
     }
 
     /**
diff --git a/core/java/android/widget/ToastPresenter.java b/core/java/android/widget/ToastPresenter.java
index eccff06..7467100 100644
--- a/core/java/android/widget/ToastPresenter.java
+++ b/core/java/android/widget/ToastPresenter.java
@@ -25,6 +25,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
@@ -55,6 +56,8 @@
 
     @VisibleForTesting
     public static final int TEXT_TOAST_LAYOUT = R.layout.transient_notification;
+    @VisibleForTesting
+    public static final int TEXT_TOAST_LAYOUT_WITH_ICON = R.layout.transient_notification_with_icon;
 
     /**
      * Returns the default text toast view for message {@code text}.
@@ -66,6 +69,24 @@
         return view;
     }
 
+    /**
+     * Returns the default icon text toast view for message {@code text} and the icon {@code icon}.
+     */
+    public static View getTextToastViewWithIcon(Context context, CharSequence text, Drawable icon) {
+        if (icon == null) {
+            return getTextToastView(context, text);
+        }
+
+        View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT_WITH_ICON, null);
+        TextView textView = view.findViewById(com.android.internal.R.id.message);
+        textView.setText(text);
+        ImageView imageView = view.findViewById(com.android.internal.R.id.icon);
+        if (imageView != null) {
+            imageView.setImageDrawable(icon);
+        }
+        return view;
+    }
+
     private final Context mContext;
     private final Resources mResources;
     private final WindowManager mWindowManager;
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 6a421f0..285258a 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -473,6 +473,13 @@
     }
     optional PowerMenuPrivacy power_menu_privacy = 81;
 
+    message ExtraLowPowerMode {
+        option (android.msg_privacy).dest = DEST_EXPLICIT;
+
+        optional SettingProto extra_automatic_power_save_mode = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    }
+    optional ExtraLowPowerMode extra_low_power_mode = 93;
+
     message PrintService {
         option (android.msg_privacy).dest = DEST_EXPLICIT;
 
@@ -679,5 +686,5 @@
 
     // Please insert fields in alphabetical order and group them into messages
     // if possible (to avoid reaching the method limit).
-    // Next tag = 93;
+    // Next tag = 94;
 }
diff --git a/core/proto/android/server/powermanagerservice.proto b/core/proto/android/server/powermanagerservice.proto
index 04f4d7b..bd4f990 100644
--- a/core/proto/android/server/powermanagerservice.proto
+++ b/core/proto/android/server/powermanagerservice.proto
@@ -238,6 +238,7 @@
         optional bool is_stay_on_while_plugged_in_ac = 1;
         optional bool is_stay_on_while_plugged_in_usb = 2;
         optional bool is_stay_on_while_plugged_in_wireless = 3;
+        optional bool is_stay_on_while_plugged_in_dock = 4;
     }
     message ScreenBrightnessSettingLimitsProto {
         option (.android.msg_privacy).dest = DEST_AUTOMATIC;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5e99d6c4..ce372e7 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7068,6 +7068,10 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
+        <service android:name="com.android.server.notification.NotificationHistoryJobService"
+                 android:permission="android.permission.BIND_JOB_SERVICE" >
+        </service>
+
         <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
             android:exported="false">
             <intent-filter>
diff --git a/core/res/res/layout/transient_notification_with_icon.xml b/core/res/res/layout/transient_notification_with_icon.xml
new file mode 100644
index 0000000..e9b17df
--- /dev/null
+++ b/core/res/res/layout/transient_notification_with_icon.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:maxWidth="@dimen/toast_width"
+    android:background="?android:attr/toastFrameBackground"
+    android:elevation="@dimen/toast_elevation"
+    android:layout_marginEnd="16dp"
+    android:layout_marginStart="16dp"
+    android:paddingStart="16dp"
+    android:paddingEnd="16dp">
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <TextView
+        android:id="@android:id/message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="2"
+        android:paddingTop="12dp"
+        android:paddingBottom="12dp"
+        android:textAppearance="@style/TextAppearance.Toast"/>
+</LinearLayout>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index a8c7bf2..067dc86 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3498,6 +3498,22 @@
     <!-- Specify if the fingerprint hardware support gestures-->
     <bool name="config_fingerprintSupportsGestures">false</bool>
 
+    <!-- The time (in millis) to wait for a power button before sending
+         a successful auth in biometric prompt(for side fingerprint) -->
+    <integer name="config_sidefpsBpPowerPressWindow">300</integer>
+
+    <!-- The time (in millis) to wait for a power button before sending a
+         successful auth in to keyguard(for side fingerprint) -->
+    <integer name="config_sidefpsKeyguardPowerPressWindow">300</integer>
+
+    <!-- The time (in millis) that a power event will ignore future authentications
+         (for side fingerprint) -->
+    <integer name="config_sidefpsPostAuthDowntime">400</integer>
+
+    <!-- The time (in millis) that a finger tap will wait for a power button
+         before dismissing the power dialog during enrollment(for side fingerprint) -->
+    <integer name="config_sidefpsEnrollPowerPressWindow">300</integer>
+
     <!-- This config is used to force VoiceInteractionService to start on certain low ram devices.
          It declares the package name of VoiceInteractionService that should be started. -->
     <string translatable="false" name="config_forceVoiceInteractionServicePackage"></string>
@@ -4374,6 +4390,9 @@
     <!-- Name of a font family to use for medium body text. -->
     <string name="config_bodyFontFamilyMedium" translatable="false">sans-serif-medium</string>
 
+    <!-- Name of the font family to use in the default lockscreen clock -->
+    <string name="config_clockFontFamily" translatable="false">monospace</string>
+
     <!-- Size of icon shown beside a preference locked by admin -->
     <dimen name="config_restrictedIconSize">@dimen/restricted_icon_size_material</dimen>
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index c6b60f5..d528385 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1751,6 +1751,8 @@
     <string name="fingerprint_error_security_update_required">Sensor temporarily disabled.</string>
     <!-- Generic error message shown when fingerprint needs calibration [CHAR LIMIT=150] -->
     <string name="fingerprint_error_bad_calibration">Can\u2019t use fingerprint sensor. Visit a repair provider</string>
+    <!-- Generic error message shown when the power button has been pressed. [CHAR LIMIT=150] -->
+    <string name="fingerprint_error_power_pressed">Power button pressed</string>
 
     <!-- Template to be used to name enrolled fingerprints by default. -->
     <string name="fingerprint_name_template">Finger <xliff:g id="fingerId" example="1">%d</xliff:g></string>
@@ -1838,9 +1840,9 @@
     <!-- Message shown during acquisition when the sensor is dirty [CHAR LIMIT=100] -->
     <string name="face_acquired_sensor_dirty">Clean the top of your screen, including the black bar</string>
     <!-- Message shown during acquisition when dark glasses were detected [CHAR LIMIT=75] -->
-    <string name="face_acquired_dark_glasses_detected">Your face must be fully visible</string>
+    <string name="face_acquired_dark_glasses_detected">@string/face_acquired_dark_glasses_detected_alt</string>
     <!-- Message shown during acquisition when a mouth covering was detected [CHAR LIMIT=75] -->
-    <string name="face_acquired_mouth_covering_detected">Your face must be fully visible</string>
+    <string name="face_acquired_mouth_covering_detected">@string/face_acquired_mouth_covering_detected_alt</string>
 
     <!-- Message shown during face acquisition when the sensor needs to be recalibrated [CHAR LIMIT=75] -->
     <string name="face_acquired_recalibrate_alt">Can\u2019t create your face model. Try again.</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 1438e7f..b2fd28f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1574,6 +1574,7 @@
   <java-symbol type="layout" name="time_picker_dialog" />
   <java-symbol type="layout" name="tooltip" />
   <java-symbol type="layout" name="transient_notification" />
+  <java-symbol type="layout" name="transient_notification_with_icon" />
   <java-symbol type="layout" name="voice_interaction_session" />
   <java-symbol type="layout" name="web_text_view_dropdown" />
   <java-symbol type="layout" name="webview_find" />
@@ -2621,10 +2622,15 @@
   <java-symbol type="string" name="fingerprint_recalibrate_notification_name" />
   <java-symbol type="string" name="fingerprint_recalibrate_notification_title" />
   <java-symbol type="string" name="fingerprint_recalibrate_notification_content" />
+  <java-symbol type="string" name="fingerprint_error_power_pressed" />
 
   <!-- Fingerprint config -->
   <java-symbol type="integer" name="config_fingerprintMaxTemplatesPerUser"/>
   <java-symbol type="bool" name="config_fingerprintSupportsGestures"/>
+  <java-symbol type="integer" name="config_sidefpsBpPowerPressWindow"/>
+  <java-symbol type="integer" name="config_sidefpsKeyguardPowerPressWindow"/>
+  <java-symbol type="integer" name="config_sidefpsPostAuthDowntime"/>
+  <java-symbol type="integer" name="config_sidefpsEnrollPowerPressWindow"/>
 
   <!-- Face authentication messages -->
   <java-symbol type="string" name="face_recalibrate_notification_name" />
diff --git a/core/tests/coretests/src/android/graphics/drawable/AdaptiveIconDrawableTest.java b/core/tests/coretests/src/android/graphics/drawable/AdaptiveIconDrawableTest.java
index 7338c3a..ddc05e0 100644
--- a/core/tests/coretests/src/android/graphics/drawable/AdaptiveIconDrawableTest.java
+++ b/core/tests/coretests/src/android/graphics/drawable/AdaptiveIconDrawableTest.java
@@ -204,6 +204,19 @@
         assertEquals(100, Color.alpha(bitmap.getPixel(50, 50)));
     }
 
+    @Test
+    public void testSetBounds() throws Exception {
+        mIconDrawable = new AdaptiveIconDrawable(mBackgroundDrawable, mForegroundDrawable);
+        mIconDrawable.setBounds(0, 0, 100, 100);
+
+        assertEquals(new Rect(-25, -25, 125, 125), mBackgroundDrawable.getBounds());
+        assertEquals(new Rect(-25, -25, 125, 125), mForegroundDrawable.getBounds());
+
+        mIconDrawable.setBounds(10, 10, 110, 110);
+        assertEquals(new Rect(-15, -15, 135, 135), mBackgroundDrawable.getBounds());
+        assertEquals(new Rect(-15, -15, 135, 135), mForegroundDrawable.getBounds());
+    }
+
     //
     // Utils
     //
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index f01e2e8..c6d9eba 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -3139,6 +3139,12 @@
       "group": "WM_DEBUG_LOCKTASK",
       "at": "com\/android\/server\/wm\/LockTaskController.java"
     },
+    "956467125": {
+      "message": "Reparenting Activity to embedded TaskFragment, but the Activity is not collected",
+      "level": "WARN",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/WindowOrganizerController.java"
+    },
     "958338552": {
       "message": "grantEmbeddedWindowFocus win=%s dropped focus so setting focus to null since no candidate was found",
       "level": "VERBOSE",
diff --git a/graphics/java/android/graphics/drawable/AdaptiveIconDrawable.java b/graphics/java/android/graphics/drawable/AdaptiveIconDrawable.java
index 2f56b18..6939a720 100644
--- a/graphics/java/android/graphics/drawable/AdaptiveIconDrawable.java
+++ b/graphics/java/android/graphics/drawable/AdaptiveIconDrawable.java
@@ -26,8 +26,6 @@
 import android.content.res.Resources;
 import android.content.res.Resources.Theme;
 import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.BitmapShader;
 import android.graphics.BlendMode;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -39,8 +37,6 @@
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.Region;
-import android.graphics.Shader;
-import android.graphics.Shader.TileMode;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.PathParser;
@@ -106,7 +102,8 @@
     private static final float DEFAULT_VIEW_PORT_SCALE = 1f / (1 + 2 * EXTRA_INSET_PERCENTAGE);
 
     /**
-     * Clip path defined in R.string.config_icon_mask.
+     * Unused path.
+     * TODO: Remove once the layoutLib is updated
      */
     private static Path sMask;
 
@@ -114,9 +111,10 @@
      * Scaled mask based on the view bounds.
      */
     private final Path mMask;
-    private final Path mMaskScaleOnly;
+    private final Path mMaskTransformed;
     private final Matrix mMaskMatrix;
     private final Region mTransparentRegion;
+    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 
     /**
      * Indices used to access {@link #mLayerState.mChildDrawable} array for foreground and
@@ -131,19 +129,10 @@
      */
     LayerState mLayerState;
 
-    private Shader mLayersShader;
-    private Bitmap mLayersBitmap;
-
     private final Rect mTmpOutRect = new Rect();
     private Rect mHotspotBounds;
     private boolean mMutated;
 
-    private boolean mSuspendChildInvalidation;
-    private boolean mChildRequestedInvalidation;
-    private final Canvas mCanvas;
-    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG |
-        Paint.FILTER_BITMAP_FLAG);
-
     /**
      * Constructor used for xml inflation.
      */
@@ -157,19 +146,16 @@
      */
     AdaptiveIconDrawable(@Nullable LayerState state, @Nullable Resources res) {
         mLayerState = createConstantState(state, res);
-        // config_icon_mask from context bound resource may have been chaged using
+        // config_icon_mask from context bound resource may have been changed using
         // OverlayManager. Read that one first.
         Resources r = ActivityThread.currentActivityThread() == null
                 ? Resources.getSystem()
                 : ActivityThread.currentActivityThread().getApplication().getResources();
-        // TODO: either make sMask update only when config_icon_mask changes OR
-        // get rid of it all-together in layoutlib
-        sMask = PathParser.createPathFromPathData(r.getString(R.string.config_icon_mask));
-        mMask = new Path(sMask);
-        mMaskScaleOnly = new Path(mMask);
+        mMask = PathParser.createPathFromPathData(r.getString(R.string.config_icon_mask));
+        mMaskTransformed = new Path();
         mMaskMatrix = new Matrix();
-        mCanvas = new Canvas();
         mTransparentRegion = new Region();
+        mPaint.setColor(Color.BLACK);
     }
 
     private ChildDrawable createChildDrawable(Drawable drawable) {
@@ -280,7 +266,7 @@
      * @return the mask path object used to clip the drawable
      */
     public Path getIconMask() {
-        return mMask;
+        return mMaskTransformed;
     }
 
     /**
@@ -322,92 +308,47 @@
         if (bounds.isEmpty()) {
             return;
         }
-        updateLayerBounds(bounds);
-    }
-
-    private void updateLayerBounds(Rect bounds) {
-        if (bounds.isEmpty()) {
-            return;
-        }
-        try {
-            suspendChildInvalidation();
-            updateLayerBoundsInternal(bounds);
-            updateMaskBoundsInternal(bounds);
-        } finally {
-            resumeChildInvalidation();
-        }
-    }
-
-    /**
-     * Set the child layer bounds bigger than the view port size by {@link #DEFAULT_VIEW_PORT_SCALE}
-     */
-    private void updateLayerBoundsInternal(Rect bounds) {
-        int cX = bounds.width() / 2;
-        int cY = bounds.height() / 2;
+        // Set the child layer bounds bigger than the view port size
+        // by {@link #DEFAULT_VIEW_PORT_SCALE}
+        float cX = bounds.exactCenterX();
+        float cY = bounds.exactCenterY();
+        float insetWidth = bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2);
+        float insetHeight = bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2);
+        final Rect outRect = mTmpOutRect;
+        outRect.set(
+                (int) (cX - insetWidth),
+                (int) (cY - insetHeight),
+                (int) (cX + insetWidth),
+                (int) (cY + insetHeight));
 
         for (int i = 0, count = mLayerState.N_CHILDREN; i < count; i++) {
             final ChildDrawable r = mLayerState.mChildren[i];
-            final Drawable d = r.mDrawable;
-            if (d == null) {
-                continue;
+            if (r.mDrawable != null) {
+                r.mDrawable.setBounds(outRect);
             }
-
-            int insetWidth = (int) (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2));
-            int insetHeight = (int) (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2));
-            final Rect outRect = mTmpOutRect;
-            outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight);
-
-            d.setBounds(outRect);
-        }
-    }
-
-    private void updateMaskBoundsInternal(Rect b) {
-        // reset everything that depends on the view bounds
-        mMaskMatrix.setScale(b.width() / MASK_SIZE, b.height() / MASK_SIZE);
-        sMask.transform(mMaskMatrix, mMaskScaleOnly);
-
-        mMaskMatrix.postTranslate(b.left, b.top);
-        sMask.transform(mMaskMatrix, mMask);
-
-        if (mLayersBitmap == null || mLayersBitmap.getWidth() != b.width()
-                || mLayersBitmap.getHeight() != b.height()) {
-            mLayersBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888);
         }
 
-        mPaint.setShader(null);
+        // Update the clipping mask
+        mMaskMatrix.setScale(bounds.width() / MASK_SIZE, bounds.height() / MASK_SIZE);
+        mMaskMatrix.postTranslate(bounds.left, bounds.top);
+        mMask.transform(mMaskMatrix, mMaskTransformed);
+
+        // Clear the transparent region, it is calculated lazily
         mTransparentRegion.setEmpty();
-        mLayersShader = null;
     }
 
     @Override
     public void draw(Canvas canvas) {
-        if (mLayersBitmap == null) {
-            return;
+        int saveCount = canvas.save();
+        canvas.clipPath(mMaskTransformed);
+        canvas.drawPaint(mPaint);
+        if (mLayerState.mChildren[BACKGROUND_ID].mDrawable != null) {
+            mLayerState.mChildren[BACKGROUND_ID].mDrawable.draw(canvas);
         }
-        if (mLayersShader == null) {
-            mCanvas.setBitmap(mLayersBitmap);
-            mCanvas.drawColor(Color.BLACK);
-            if (mLayerState.mChildren[BACKGROUND_ID].mDrawable != null) {
-                mLayerState.mChildren[BACKGROUND_ID].mDrawable.draw(mCanvas);
-            }
-            if (mLayerState.mChildren[FOREGROUND_ID].mDrawable != null) {
-                mLayerState.mChildren[FOREGROUND_ID].mDrawable.draw(mCanvas);
-            }
-            mLayersShader = new BitmapShader(mLayersBitmap, TileMode.CLAMP, TileMode.CLAMP);
-            mPaint.setShader(mLayersShader);
+        if (mLayerState.mChildren[FOREGROUND_ID].mDrawable != null) {
+            mLayerState.mChildren[FOREGROUND_ID].mDrawable.draw(canvas);
         }
-        if (mMaskScaleOnly != null) {
-            Rect bounds = getBounds();
-            canvas.translate(bounds.left, bounds.top);
-            canvas.drawPath(mMaskScaleOnly, mPaint);
-            canvas.translate(-bounds.left, -bounds.top);
-        }
-    }
-
-    @Override
-    public void invalidateSelf() {
-        mLayersShader = null;
-        super.invalidateSelf();
+        canvas.restoreToCount(saveCount);
     }
 
     @Override
@@ -600,37 +541,9 @@
         return false;
     }
 
-    /**
-     * Temporarily suspends child invalidation.
-     *
-     * @see #resumeChildInvalidation()
-     */
-    private void suspendChildInvalidation() {
-        mSuspendChildInvalidation = true;
-    }
-
-    /**
-     * Resumes child invalidation after suspension, immediately performing an
-     * invalidation if one was requested by a child during suspension.
-     *
-     * @see #suspendChildInvalidation()
-     */
-    private void resumeChildInvalidation() {
-        mSuspendChildInvalidation = false;
-
-        if (mChildRequestedInvalidation) {
-            mChildRequestedInvalidation = false;
-            invalidateSelf();
-        }
-    }
-
     @Override
     public void invalidateDrawable(@NonNull Drawable who) {
-        if (mSuspendChildInvalidation) {
-            mChildRequestedInvalidation = true;
-        } else {
-            invalidateSelf();
-        }
+        invalidateSelf();
     }
 
     @Override
@@ -714,6 +627,13 @@
     @Override
     public void setAlpha(int alpha) {
         mPaint.setAlpha(alpha);
+        final ChildDrawable[] array = mLayerState.mChildren;
+        for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+            final Drawable dr = array[i].mDrawable;
+            if (dr != null) {
+                dr.setAlpha(alpha);
+            }
+        }
     }
 
     @Override
@@ -816,10 +736,6 @@
             }
         }
 
-        if (changed) {
-            updateLayerBounds(getBounds());
-        }
-
         return changed;
     }
 
@@ -835,10 +751,6 @@
             }
         }
 
-        if (changed) {
-            updateLayerBounds(getBounds());
-        }
-
         return changed;
     }
 
@@ -979,6 +891,7 @@
         int mDensity;
 
         // The density to use when inflating/looking up the children drawables. A value of 0 means
+
         // use the system's density.
         int mSrcDensityOverride = 0;
 
diff --git a/identity/java/android/security/identity/Util.java b/identity/java/android/security/identity/Util.java
index e56bd51..789ff06 100644
--- a/identity/java/android/security/identity/Util.java
+++ b/identity/java/android/security/identity/Util.java
@@ -20,12 +20,12 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.math.BigInteger;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
 import java.security.interfaces.ECPublicKey;
 import java.security.spec.ECPoint;
-import java.util.Collection;
 
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
@@ -36,15 +36,6 @@
 public class Util {
     private static final String TAG = "Util";
 
-    static int[] integerCollectionToArray(Collection<Integer> collection) {
-        int[] result = new int[collection.size()];
-        int n = 0;
-        for (int item : collection) {
-            result[n++] = item;
-        }
-        return result;
-    }
-
     static byte[] stripLeadingZeroes(byte[] value) {
         int n = 0;
         while (n < value.length && value[n] == 0) {
@@ -61,15 +52,47 @@
 
     static byte[] publicKeyEncodeUncompressedForm(PublicKey publicKey) {
         ECPoint w = ((ECPublicKey) publicKey).getW();
-        // X and Y are always positive so for interop we remove any leading zeroes
-        // inserted by the BigInteger encoder.
-        byte[] x = stripLeadingZeroes(w.getAffineX().toByteArray());
-        byte[] y = stripLeadingZeroes(w.getAffineY().toByteArray());
+        BigInteger x = w.getAffineX();
+        BigInteger y = w.getAffineY();
+        if (x.compareTo(BigInteger.ZERO) < 0) {
+            throw new RuntimeException("X is negative");
+        }
+        if (y.compareTo(BigInteger.ZERO) < 0) {
+            throw new RuntimeException("Y is negative");
+        }
         try {
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             baos.write(0x04);
-            baos.write(x);
-            baos.write(y);
+
+            // Each coordinate may be encoded in 33*, 32, or fewer bytes.
+            //
+            //  * : it can be 33 bytes because toByteArray() guarantees "The array will contain the
+            //      minimum number of bytes required to represent this BigInteger, including at
+            //      least one sign bit, which is (ceil((this.bitLength() + 1)/8))" which means that
+            //      the MSB is always 0x00. This is taken care of by calling calling
+            //      stripLeadingZeroes().
+            //
+            // We need the encoding to be exactly 32 bytes since according to RFC 5480 section 2.2
+            // and SEC 1: Elliptic Curve Cryptography section 2.3.3 the encoding is 0x04 | X | Y
+            // where X and Y are encoded in exactly 32 byte, big endian integer values each.
+            //
+            byte[] xBytes = stripLeadingZeroes(x.toByteArray());
+            if (xBytes.length > 32) {
+                throw new RuntimeException("xBytes is " + xBytes.length + " which is unexpected");
+            }
+            for (int n = 0; n < 32 - xBytes.length; n++) {
+                baos.write(0x00);
+            }
+            baos.write(xBytes);
+
+            byte[] yBytes = stripLeadingZeroes(y.toByteArray());
+            if (yBytes.length > 32) {
+                throw new RuntimeException("yBytes is " + yBytes.length + " which is unexpected");
+            }
+            for (int n = 0; n < 32 - yBytes.length; n++) {
+                baos.write(0x00);
+            }
+            baos.write(yBytes);
             return baos.toByteArray();
         } catch (IOException e) {
             throw new RuntimeException("Unexpected IOException", e);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index f85f9d6..5dd5149 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -411,8 +411,8 @@
 
     @WMSingleton
     @Provides
-    static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper() {
-        return new PipSurfaceTransactionHelper();
+    static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper(Context context) {
+        return new PipSurfaceTransactionHelper(context);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
index cc741d3..35a309a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
@@ -27,6 +27,7 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Trace;
+import android.view.Choreographer;
 
 import androidx.annotation.Nullable;
 
@@ -144,6 +145,25 @@
     }
 
     /**
+     * Provide a Shell main-thread {@link Choreographer} with the app vsync.
+     *
+     * @param executor the executor of the shell main thread
+     */
+    @WMSingleton
+    @Provides
+    @ShellMainThread
+    public static Choreographer provideShellMainChoreographer(
+            @ShellMainThread ShellExecutor executor) {
+        try {
+            final Choreographer[] choreographer = new Choreographer[1];
+            executor.executeBlocking(() -> choreographer[0] = Choreographer.getInstance());
+            return choreographer[0];
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Failed to obtain main Choreographer.", e);
+        }
+    }
+
+    /**
      * Provide a Shell animation-thread Executor.
      */
     @WMSingleton
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 0f33f4c..0dc293b 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
@@ -20,6 +20,7 @@
 import android.content.pm.LauncherApps;
 import android.os.Handler;
 import android.os.UserManager;
+import android.view.Choreographer;
 import android.view.WindowManager;
 
 import com.android.internal.jank.InteractionJankMonitor;
@@ -173,12 +174,14 @@
     static WindowDecorViewModel<?> provideWindowDecorViewModel(
             Context context,
             @ShellMainThread Handler mainHandler,
+            @ShellMainThread Choreographer mainChoreographer,
             ShellTaskOrganizer taskOrganizer,
             DisplayController displayController,
             SyncTransactionQueue syncQueue) {
         return new CaptionWindowDecorViewModel(
                         context,
                         mainHandler,
+                        mainChoreographer,
                         taskOrganizer,
                         displayController,
                         syncQueue);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index c0bc108..3ac08a6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -39,6 +39,10 @@
     private int mCornerRadius;
     private int mShadowRadius;
 
+    public PipSurfaceTransactionHelper(Context context) {
+        onDensityOrFontScaleChanged(context);
+    }
+
     /**
      * Called when display size or font size of settings changed
      *
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index da88c2d..1155ea1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -62,6 +62,7 @@
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.util.Log;
 import android.view.Display;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -444,7 +445,7 @@
             // When exit to fullscreen with Shell transition enabled, we update the Task windowing
             // mode directly so that it can also trigger display rotation and visibility update in
             // the same transition if there will be any.
-            wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+            wct.setWindowingMode(mToken, getOutPipWindowingMode());
             // We can inherit the parent bounds as it is going to be fullscreen. The
             // destinationBounds calculated above will be incorrect if this is with rotation.
             wct.setBounds(mToken, null);
@@ -543,7 +544,7 @@
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             final WindowContainerTransaction wct = new WindowContainerTransaction();
             wct.setBounds(mToken, null);
-            wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+            wct.setWindowingMode(mToken, getOutPipWindowingMode());
             wct.reorder(mToken, false);
             mPipTransitionController.startExitTransition(TRANSIT_REMOVE_PIP, wct,
                     null /* destinationBounds */);
@@ -930,6 +931,12 @@
 
     /** Called when exiting PIP transition is finished to do the state cleanup. */
     void onExitPipFinished(TaskInfo info) {
+        if (mLeash == null) {
+            // TODO(239461594): Remove once the double call to onExitPipFinished() is fixed
+            Log.w(TAG, "Warning, onExitPipFinished() called multiple times in the same sessino");
+            return;
+        }
+
         clearWaitForFixedRotation();
         if (mSwipePipToHomeOverlay != null) {
             removeContentOverlay(mSwipePipToHomeOverlay, null /* callback */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 53ec39d..7fb961f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -352,41 +352,31 @@
 
     public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
             @SplitPosition int position, @Nullable Bundle options) {
+        if (fillInIntent == null) {
+            fillInIntent = new Intent();
+        }
+        // Flag this as a no-user-action launch to prevent sending user leaving event to the
+        // current top activity since it's going to be put into another side of the split. This
+        // prevents the current top activity from going into pip mode due to user leaving event.
+        fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
+
+        // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
+        // split.
+        if (isLaunchingAdjacently(intent.getIntent(), position)) {
+            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+        }
+
         if (!ENABLE_SHELL_TRANSITIONS) {
             startIntentLegacy(intent, fillInIntent, position, options);
             return;
         }
 
-        try {
-            options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
-                    null /* wct */);
-
-            if (fillInIntent == null) {
-                fillInIntent = new Intent();
-            }
-            // Flag this as a no-user-action launch to prevent sending user leaving event to the
-            // current top activity since it's going to be put into another side of the split. This
-            // prevents the current top activity from going into pip mode due to user leaving event.
-            fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
-
-            // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
-            // split.
-            if (isLaunchingAdjacently(intent.getIntent(), position)) {
-                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
-            }
-
-            intent.send(mContext, 0, fillInIntent, null /* onFinished */, null /* handler */,
-                    null /* requiredPermission */, options);
-        } catch (PendingIntent.CanceledException e) {
-            Slog.e(TAG, "Failed to launch task", e);
-        }
+        mStageCoordinator.startIntent(intent, fillInIntent, position, options);
     }
 
-    private void startIntentLegacy(PendingIntent intent, @Nullable Intent fillInIntent,
+    private void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
             @SplitPosition int position, @Nullable Bundle options) {
-        boolean startSameActivityAdjacently = isLaunchingAdjacently(intent.getIntent(), position);
-
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         mStageCoordinator.prepareEvictChildTasks(position, evictWct);
 
@@ -397,8 +387,8 @@
                     IRemoteAnimationFinishedCallback finishedCallback,
                     SurfaceControl.Transaction t) {
                 if (apps == null || apps.length == 0) {
-                    if (startSameActivityAdjacently) {
-                        // Switch split position if dragging the same activity to another side.
+                    // Switch the split position if launching as MULTIPLE_TASK failed.
+                    if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
                         setSideStagePosition(SplitLayout.reversePosition(
                                 mStageCoordinator.getSideStagePosition()));
                     }
@@ -408,8 +398,6 @@
                     return;
                 }
 
-                mStageCoordinator.updateSurfaceBounds(null /* layout */, t,
-                        false /* applyResizingOffset */);
                 for (int i = 0; i < apps.length; ++i) {
                     if (apps[i].mode == MODE_OPENING) {
                         t.show(apps[i].leash);
@@ -432,18 +420,6 @@
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct);
 
-        // Flag this as a no-user-action launch to prevent sending user leaving event to the current
-        // top activity since it's going to be put into another side of the split. This prevents the
-        // current top activity from going into pip mode due to user leaving event.
-        if (fillInIntent == null) {
-            fillInIntent = new Intent();
-        }
-        fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
-        if (startSameActivityAdjacently) {
-            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
-        }
-
         wct.sendPendingIntent(intent, fillInIntent, options);
         mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 056cd58..83bdf8b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -62,13 +62,12 @@
     private final Runnable mOnFinish;
 
     DismissTransition mPendingDismiss = null;
-    IBinder mPendingEnter = null;
-    IBinder mPendingRecent = null;
+    TransitSession mPendingEnter = null;
+    TransitSession mPendingRecent = null;
 
     private IBinder mAnimatingTransition = null;
     OneShotRemoteHandler mPendingRemoteHandler = null;
     private OneShotRemoteHandler mActiveRemoteHandler = null;
-    private boolean mEnterTransitionMerged;
 
     private final Transitions.TransitionFinishCallback mRemoteFinishCB = this::onFinish;
 
@@ -145,7 +144,7 @@
                 continue;
             }
 
-            if (transition == mPendingEnter && (mainRoot.equals(change.getContainer())
+            if (isPendingEnter(transition) && (mainRoot.equals(change.getContainer())
                     || sideRoot.equals(change.getContainer()))) {
                 t.setPosition(leash, change.getEndAbsBounds().left, change.getEndAbsBounds().top);
                 t.setWindowCrop(leash, change.getEndAbsBounds().width(),
@@ -171,12 +170,40 @@
         onFinish(null /* wct */, null /* wctCB */);
     }
 
+    boolean isPendingTransition(IBinder transition) {
+        return isPendingEnter(transition)
+                || isPendingDismiss(transition)
+                || isPendingRecent(transition);
+    }
+
+    boolean isPendingEnter(IBinder transition) {
+        return mPendingEnter != null && mPendingEnter.mTransition == transition;
+    }
+
+    boolean isPendingRecent(IBinder transition) {
+        return mPendingRecent != null && mPendingRecent.mTransition == transition;
+    }
+
+    boolean isPendingDismiss(IBinder transition) {
+        return mPendingDismiss != null && mPendingDismiss.mTransition == transition;
+    }
+
     /** Starts a transition to enter split with a remote transition animator. */
-    IBinder startEnterTransition(@WindowManager.TransitionType int transitType,
-            @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition,
-            @NonNull Transitions.TransitionHandler handler) {
+    IBinder startEnterTransition(
+            @WindowManager.TransitionType int transitType,
+            WindowContainerTransaction wct,
+            @Nullable RemoteTransition remoteTransition,
+            Transitions.TransitionHandler handler,
+            @Nullable TransitionCallback callback) {
         final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
-        mPendingEnter = transition;
+        setEnterTransition(transition, remoteTransition, callback);
+        return transition;
+    }
+
+    /** Sets a transition to enter split. */
+    void setEnterTransition(@NonNull IBinder transition,
+            @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) {
+        mPendingEnter = new TransitSession(transition, callback);
 
         if (remoteTransition != null) {
             // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
@@ -184,7 +211,9 @@
                     mTransitions.getMainExecutor(), remoteTransition);
             mPendingRemoteHandler.setTransition(transition);
         }
-        return transition;
+
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  splitTransition "
+                + " deduced Enter split screen");
     }
 
     /** Starts a transition to dismiss split. */
@@ -209,8 +238,8 @@
     }
 
     void setRecentTransition(@NonNull IBinder transition,
-            @Nullable RemoteTransition remoteTransition) {
-        mPendingRecent = transition;
+            @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) {
+        mPendingRecent = new TransitSession(transition, callback);
 
         if (remoteTransition != null) {
             // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
@@ -226,6 +255,18 @@
     void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t,
             IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) {
         if (mergeTarget != mAnimatingTransition) return;
+
+        if (isPendingEnter(transition) && isPendingRecent(mergeTarget)) {
+            mPendingRecent.mCallback = new TransitionCallback() {
+                @Override
+                public void onTransitionFinished(WindowContainerTransaction finishWct,
+                        SurfaceControl.Transaction finishT) {
+                    // Since there's an entering transition merged, recent transition no longer
+                    // need to handle entering split screen after the transition finished.
+                }
+            };
+        }
+
         if (mActiveRemoteHandler != null) {
             mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
         } else {
@@ -247,38 +288,55 @@
     }
 
     void onTransitionConsumed(@NonNull IBinder transition, boolean aborted) {
-        if (aborted) return;
+        if (isPendingEnter(transition)) {
+            if (!aborted) {
+                // An enter transition got merged, appends the rest operations to finish entering
+                // split screen.
+                // TODO (b/238856352): Passed-in the proper finish transition to merge instead.
+                if (mFinishTransaction == null) {
+                    mFinishTransaction = mTransactionPool.acquire();
+                }
+                mStageCoordinator.finishEnterSplitScreen(mFinishTransaction);
+            }
 
-        // Once a pending enter transition got merged, make sure to append the reset of finishing
-        // operations to the finish transition.
-        if (transition == mPendingEnter) {
-            mFinishTransaction = mTransactionPool.acquire();
-            mStageCoordinator.finishEnterSplitScreen(mFinishTransaction);
+            mPendingEnter.mCallback.onTransitionConsumed(aborted);
             mPendingEnter = null;
             mPendingRemoteHandler = null;
-            mEnterTransitionMerged = true;
+        } else if (isPendingDismiss(transition)) {
+            mPendingDismiss.mCallback.onTransitionConsumed(aborted);
+            mPendingDismiss = null;
+        } else if (isPendingRecent(transition)) {
+            mPendingRecent.mCallback.onTransitionConsumed(aborted);
+            mPendingRecent = null;
+            mPendingRemoteHandler = null;
         }
     }
 
     void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) {
         if (!mAnimations.isEmpty()) return;
-        if (mAnimatingTransition == mPendingEnter) {
+
+        TransitionCallback callback = null;
+        if (isPendingEnter(mAnimatingTransition)) {
+            callback = mPendingEnter.mCallback;
             mPendingEnter = null;
         }
-        if (mPendingDismiss != null && mPendingDismiss.mTransition == mAnimatingTransition) {
+        if (isPendingDismiss(mAnimatingTransition)) {
+            callback = mPendingDismiss.mCallback;
             mPendingDismiss = null;
         }
-        if (mAnimatingTransition == mPendingRecent) {
-            if (!mEnterTransitionMerged) {
-                if (wct == null) wct = new WindowContainerTransaction();
-                mStageCoordinator.onRecentTransitionFinished(wct, mFinishTransaction);
-            }
+        if (isPendingRecent(mAnimatingTransition)) {
+            callback = mPendingRecent.mCallback;
             mPendingRecent = null;
         }
+
+        if (callback != null) {
+            if (wct == null) wct = new WindowContainerTransaction();
+            callback.onTransitionFinished(wct, mFinishTransaction);
+        }
+
         mPendingRemoteHandler = null;
         mActiveRemoteHandler = null;
         mAnimatingTransition = null;
-        mEnterTransitionMerged = false;
 
         mOnFinish.run();
         if (mFinishTransaction != null) {
@@ -382,17 +440,34 @@
                 || info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
     }
 
+    /** Clean-up callbacks for transition. */
+    interface TransitionCallback {
+        /** Calls when the transition got consumed. */
+        default void onTransitionConsumed(boolean aborted) {}
+
+        /** Calls when the transition finished. */
+        default void onTransitionFinished(WindowContainerTransaction finishWct,
+                SurfaceControl.Transaction finishT) {}
+    }
+
+    /** Session for a transition and its clean-up callback. */
+    static class TransitSession {
+        final IBinder mTransition;
+        TransitionCallback mCallback;
+
+        TransitSession(IBinder transition, @Nullable TransitionCallback callback) {
+            mTransition = transition;
+            mCallback = callback != null ? callback : new TransitionCallback() {};
+        }
+    }
+
     /** Bundled information of dismiss transition. */
-    static class DismissTransition {
-        IBinder mTransition;
-
-        int mReason;
-
-        @SplitScreen.StageType
-        int mDismissTop;
+    static class DismissTransition extends TransitSession {
+        final int mReason;
+        final @SplitScreen.StageType int mDismissTop;
 
         DismissTransition(IBinder transition, int reason, int dismissTop) {
-            this.mTransition = transition;
+            super(transition, null /* callback */);
             this.mReason = reason;
             this.mDismissTop = dismissTop;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 2229e26..3c7db33 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -24,6 +24,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -214,6 +215,33 @@
                 }
             };
 
+    private final SplitScreenTransitions.TransitionCallback mRecentTransitionCallback =
+            new SplitScreenTransitions.TransitionCallback() {
+        @Override
+        public void onTransitionFinished(WindowContainerTransaction finishWct,
+                SurfaceControl.Transaction finishT) {
+            // Check if the recent transition is finished by returning to the current split, so we
+            // can restore the divider bar.
+            for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
+                final WindowContainerTransaction.HierarchyOp op =
+                        finishWct.getHierarchyOps().get(i);
+                final IBinder container = op.getContainer();
+                if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
+                        && (mMainStage.containsContainer(container)
+                        || mSideStage.containsContainer(container))) {
+                    setDividerVisibility(true, finishT);
+                    return;
+                }
+            }
+
+            // Dismiss the split screen if it's not returning to split.
+            prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct);
+            setSplitsVisible(false);
+            setDividerVisibility(false, finishT);
+            logExit(EXIT_REASON_UNKNOWN);
+        }
+    };
+
     StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
             ShellTaskOrganizer taskOrganizer, DisplayController displayController,
             DisplayImeController displayImeController,
@@ -337,15 +365,23 @@
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         targetStage.evictAllChildren(evictWct);
         targetStage.addTask(task, wct);
-        if (!evictWct.isEmpty()) {
-            wct.merge(evictWct, true /* transfer */);
-        }
 
         if (ENABLE_SHELL_TRANSITIONS) {
             prepareEnterSplitScreen(wct);
-            mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE,
-                    wct, null, this);
+            mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct,
+                    null, this, new SplitScreenTransitions.TransitionCallback() {
+                        @Override
+                        public void onTransitionFinished(WindowContainerTransaction finishWct,
+                                SurfaceControl.Transaction finishT) {
+                            if (!evictWct.isEmpty()) {
+                                finishWct.merge(evictWct, true);
+                            }
+                        }
+                    });
         } else {
+            if (!evictWct.isEmpty()) {
+                wct.merge(evictWct, true /* transfer */);
+            }
             mTaskOrganizer.applyTransaction(wct);
         }
         return true;
@@ -365,6 +401,39 @@
         return result;
     }
 
+    /** Launches an activity into split. */
+    void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
+            @Nullable Bundle options) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+        prepareEvictChildTasks(position, evictWct);
+
+        options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
+        wct.sendPendingIntent(intent, fillInIntent, options);
+        prepareEnterSplitScreen(wct, null /* taskInfo */, position);
+
+        mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, null, this,
+                new SplitScreenTransitions.TransitionCallback() {
+                    @Override
+                    public void onTransitionConsumed(boolean aborted) {
+                        // Switch the split position if launching as MULTIPLE_TASK failed.
+                        if (aborted
+                                && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
+                            setSideStagePositionAnimated(
+                                    SplitLayout.reversePosition(mSideStagePosition));
+                        }
+                    }
+
+                    @Override
+                    public void onTransitionFinished(WindowContainerTransaction finishWct,
+                            SurfaceControl.Transaction finishT) {
+                        if (!evictWct.isEmpty()) {
+                            finishWct.merge(evictWct, true);
+                        }
+                    }
+                });
+    }
+
     /** Starts 2 tasks in one transition. */
     void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId,
             @Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio,
@@ -395,7 +464,7 @@
         wct.startTask(sideTaskId, sideOptions);
 
         mSplitTransitions.startEnterTransition(
-                TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this);
+                TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null);
     }
 
     /** Starts 2 tasks in one legacy transition. */
@@ -617,11 +686,13 @@
     }
 
     int getTaskId(@SplitPosition int splitPosition) {
-        if (mSideStagePosition == splitPosition) {
-            return mSideStage.getTopVisibleChildTaskId();
-        } else {
-            return mMainStage.getTopVisibleChildTaskId();
+        if (splitPosition == SPLIT_POSITION_UNDEFINED) {
+            return INVALID_TASK_ID;
         }
+
+        return mSideStagePosition == splitPosition
+                ? mSideStage.getTopVisibleChildTaskId()
+                : mMainStage.getTopVisibleChildTaskId();
     }
 
     void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) {
@@ -861,6 +932,7 @@
         mSplitLayout.init();
         setDividerVisibility(true, t);
         updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+        t.show(mRootTaskLeash);
         setSplitsVisible(true);
         mShouldUpdateRecents = true;
         updateRecentTasksSplitPair();
@@ -1211,7 +1283,7 @@
     private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
         final boolean hasChildren = stageListener.mHasChildren;
         final boolean isSideStage = stageListener == mSideStageListener;
-        if (!hasChildren && !mIsExiting) {
+        if (!hasChildren && !mIsExiting && mMainStage.isActive()) {
             if (isSideStage && mMainStageListener.mVisible) {
                 // Exit to main stage if side stage no longer has children.
                 if (ENABLE_SHELL_TRANSITIONS) {
@@ -1231,7 +1303,7 @@
                             EXIT_REASON_APP_FINISHED);
                 }
             }
-        } else if (isSideStage && !mMainStage.isActive()) {
+        } else if (isSideStage && hasChildren && !mMainStage.isActive()) {
             if (mFocusingTaskInfo != null && !isValidToEnterSplitScreen(mFocusingTaskInfo)) {
                 final WindowContainerTransaction wct = new WindowContainerTransaction();
                 mSideStage.removeAllTasks(wct, true);
@@ -1543,14 +1615,14 @@
                 } else if (activityType == ACTIVITY_TYPE_HOME
                         || activityType == ACTIVITY_TYPE_RECENTS) {
                     // Enter overview panel, so start recent transition.
-                    mSplitTransitions.setRecentTransition(transition,
-                            request.getRemoteTransition());
+                    mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(),
+                            mRecentTransitionCallback);
                 } else if (mSplitTransitions.mPendingRecent == null) {
                     // If split-task is not controlled by recents animation
                     // and occluded by the other fullscreen task, dismiss both.
                     prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out);
-                    mSplitTransitions.setDismissTransition(transition,
-                            STAGE_TYPE_UNDEFINED, EXIT_REASON_UNKNOWN);
+                    mSplitTransitions.setDismissTransition(
+                            transition, STAGE_TYPE_UNDEFINED, EXIT_REASON_UNKNOWN);
                 }
             }
         } else {
@@ -1558,7 +1630,8 @@
                 // One task is appearing into split, prepare to enter split screen.
                 out = new WindowContainerTransaction();
                 prepareEnterSplitScreen(out);
-                mSplitTransitions.mPendingEnter = transition;
+                mSplitTransitions.setEnterTransition(
+                        transition, request.getRemoteTransition(), null /* callback */);
             }
         }
         return out;
@@ -1614,10 +1687,7 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        if (transition != mSplitTransitions.mPendingEnter
-                && transition != mSplitTransitions.mPendingRecent
-                && (mSplitTransitions.mPendingDismiss == null
-                        || mSplitTransitions.mPendingDismiss.mTransition != transition)) {
+        if (!mSplitTransitions.isPendingTransition(transition)) {
             // Not entering or exiting, so just do some house-keeping and validation.
 
             // If we're not in split-mode, just abort so something else can handle it.
@@ -1664,12 +1734,11 @@
         }
 
         boolean shouldAnimate = true;
-        if (mSplitTransitions.mPendingEnter == transition) {
+        if (mSplitTransitions.isPendingEnter(transition)) {
             shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction);
-        } else if (mSplitTransitions.mPendingRecent == transition) {
+        } else if (mSplitTransitions.isPendingRecent(transition)) {
             shouldAnimate = startPendingRecentAnimation(transition, info, startTransaction);
-        } else if (mSplitTransitions.mPendingDismiss != null
-                && mSplitTransitions.mPendingDismiss.mTransition == transition) {
+        } else if (mSplitTransitions.isPendingDismiss(transition)) {
             shouldAnimate = startPendingDismissAnimation(
                     mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction);
         }
@@ -1837,28 +1906,6 @@
         return true;
     }
 
-    void onRecentTransitionFinished(WindowContainerTransaction wct,
-            SurfaceControl.Transaction finishT) {
-        // Check if the recent transition is finished by returning to the current split so we can
-        // restore the divider bar.
-        for (int i = 0; i < wct.getHierarchyOps().size(); ++i) {
-            final WindowContainerTransaction.HierarchyOp op = wct.getHierarchyOps().get(i);
-            final IBinder container = op.getContainer();
-            if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
-                    && (mMainStage.containsContainer(container)
-                    || mSideStage.containsContainer(container))) {
-                setDividerVisibility(true, finishT);
-                return;
-            }
-        }
-
-        // Dismiss the split screen is it's not returning to split.
-        prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
-        setSplitsVisible(false);
-        setDividerVisibility(false, finishT);
-        logExit(EXIT_REASON_UNKNOWN);
-    }
-
     private void addDividerBarToTransition(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, boolean show) {
         final SurfaceControl leash = mSplitLayout.getDividerLeash();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index dcd6277..0bec543 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -483,11 +483,11 @@
                     postStartTransactionCallbacks.add(t ->
                             startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
                                     mTransactionPool, mMainExecutor, mAnimExecutor,
-                                    null /* position */, cornerRadius, clipRect));
+                                    change.getEndRelOffset(), cornerRadius, clipRect));
                 } else {
                     startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
-                            mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */,
-                            cornerRadius, clipRect);
+                            mTransactionPool, mMainExecutor, mAnimExecutor,
+                            change.getEndRelOffset(), cornerRadius, clipRect);
                 }
 
                 if (info.getAnimationOptions() != null) {
@@ -934,7 +934,7 @@
         a.restrictDuration(MAX_ANIMATION_DURATION);
         a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
         startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
-                mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top),
+                mMainExecutor, mAnimExecutor, change.getEndRelOffset(),
                 cornerRadius, change.getEndAbsBounds());
     }
 
@@ -959,7 +959,7 @@
         a.restrictDuration(MAX_ANIMATION_DURATION);
         a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
         startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
-                mMainExecutor, mAnimExecutor, null /* position */,
+                mMainExecutor, mAnimExecutor, change.getEndRelOffset(),
                 cornerRadius, change.getEndAbsBounds());
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 6d28d73..fe3724b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -25,6 +25,7 @@
 import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.os.Handler;
+import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -45,17 +46,20 @@
     private final ShellTaskOrganizer mTaskOrganizer;
     private final Context mContext;
     private final Handler mMainHandler;
+    private final Choreographer mMainChoreographer;
     private final DisplayController mDisplayController;
     private final SyncTransactionQueue mSyncQueue;
 
     public CaptionWindowDecorViewModel(
             Context context,
             Handler mainHandler,
+            Choreographer mainChoreographer,
             ShellTaskOrganizer taskOrganizer,
             DisplayController displayController,
             SyncTransactionQueue syncQueue) {
         mContext = context;
         mMainHandler = mainHandler;
+        mMainChoreographer = mainChoreographer;
         mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
         mTaskOrganizer = taskOrganizer;
         mDisplayController = displayController;
@@ -72,6 +76,7 @@
                 taskInfo,
                 taskSurface,
                 mMainHandler,
+                mMainChoreographer,
                 mSyncQueue);
         TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration);
         CaptionTouchEventListener touchEventListener =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index cdca051..2df7104 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -25,6 +25,7 @@
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.VectorDrawable;
 import android.os.Handler;
+import android.view.Choreographer;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.window.WindowContainerTransaction;
@@ -59,6 +60,7 @@
             RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP);
 
     private final Handler mHandler;
+    private final Choreographer mChoreographer;
     private final SyncTransactionQueue mSyncQueue;
 
     private View.OnClickListener mOnCaptionButtonClickListener;
@@ -77,10 +79,12 @@
             ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
             Handler handler,
+            Choreographer choreographer,
             SyncTransactionQueue syncQueue) {
         super(context, displayController, taskOrganizer, taskInfo, taskSurface);
 
         mHandler = handler;
+        mChoreographer = choreographer;
         mSyncQueue = syncQueue;
     }
 
@@ -138,6 +142,7 @@
             mDragResizeListener = new DragResizeInputListener(
                     mContext,
                     mHandler,
+                    mChoreographer,
                     mDisplay.getDisplayId(),
                     mDecorationContainerSurface,
                     mDragResizeCallback);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index 9869d2e..f512b0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -49,6 +49,7 @@
 
     private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession();
     private final Handler mHandler;
+    private final Choreographer mChoreographer;
     private final InputManager mInputManager;
 
     private final int mDisplayId;
@@ -68,11 +69,13 @@
     DragResizeInputListener(
             Context context,
             Handler handler,
+            Choreographer choreographer,
             int displayId,
             SurfaceControl decorationSurface,
             DragResizeCallback callback) {
         mInputManager = context.getSystemService(InputManager.class);
         mHandler = handler;
+        mChoreographer = choreographer;
         mDisplayId = displayId;
         mDecorationSurface = decorationSurface;
         // Use a fake window as the backing surface is a container layer and we don't want to create
@@ -97,7 +100,8 @@
             e.rethrowFromSystemServer();
         }
 
-        mInputEventReceiver = new TaskResizeInputEventReceiver(mInputChannel, mHandler);
+        mInputEventReceiver = new TaskResizeInputEventReceiver(
+                mInputChannel, mHandler, mChoreographer);
         mCallback = callback;
     }
 
@@ -171,13 +175,10 @@
         private final Runnable mConsumeBatchEventRunnable;
         private boolean mConsumeBatchEventScheduled;
 
-        private TaskResizeInputEventReceiver(InputChannel inputChannel, Handler handler) {
+        private TaskResizeInputEventReceiver(
+                InputChannel inputChannel, Handler handler, Choreographer choreographer) {
             super(inputChannel, handler.getLooper());
-
-            final Choreographer[] choreographer = new Choreographer[1];
-            handler.runWithScissors(
-                    () -> choreographer[0] = Choreographer.getInstance(), 0);
-            mChoreographer = choreographer[0];
+            mChoreographer = choreographer;
 
             mConsumeBatchEventRunnable = () -> {
                 mConsumeBatchEventScheduled = false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index c19a33a..4855fbd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -222,10 +222,10 @@
 
         float shadowRadius = outResult.mDensity * shadowRadiusDp;
         int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
-        mTmpColor[0] = Color.red(backgroundColorInt);
-        mTmpColor[1] = Color.green(backgroundColorInt);
-        mTmpColor[2] = Color.blue(backgroundColorInt);
-        t.setCrop(mTaskBackgroundSurface, taskBounds)
+        mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
+        mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
+        mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
+        t.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height())
                 .setShadowRadius(mTaskBackgroundSurface, shadowRadius)
                 .setColor(mTaskBackgroundSurface, mTmpColor);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java
index 4922872..f8b3fb3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java
@@ -34,13 +34,12 @@
      * given {@link SurfaceControl} when calling {@link SurfaceControl.Builder#build()}.
      *
      * @param mockSurfaceControl the first {@link SurfaceControl} to return
-     * @param mockSurfaceControls following {@link SurfaceControl} to return
      * @return the mock of {@link SurfaceControl.Builder}
      */
     public static SurfaceControl.Builder createMockSurfaceControlBuilder(
-            SurfaceControl mockSurfaceControl, SurfaceControl... mockSurfaceControls) {
+            SurfaceControl mockSurfaceControl) {
         final SurfaceControl.Builder mockBuilder = mock(SurfaceControl.Builder.class, RETURNS_SELF);
-        doReturn(mockSurfaceControl, (Object[]) mockSurfaceControls)
+        doReturn(mockSurfaceControl)
                 .when(mockBuilder)
                 .build();
         return mockBuilder;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
index 0972cf2..1636c5f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
@@ -25,6 +25,7 @@
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertNotNull
 import junit.framework.Assert.assertTrue
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -61,6 +62,12 @@
         bubbles.put(1, user1Bubbles)
     }
 
+    @After
+    fun teardown() {
+        // Clean up the any persisted bubbles for the next run
+        repository.persistsToDisk(SparseArray())
+    }
+
     @Test
     fun testReadWriteOperation() {
         // Verify read before write doesn't cause FileNotFoundException
@@ -71,4 +78,4 @@
         repository.persistsToDisk(bubbles)
         assertTrue(sparseArraysEqual(bubbles, repository.readFromDisk()))
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
index 52d78ca..5880ffb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
@@ -37,6 +37,7 @@
 import android.view.SurfaceControl;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.wm.shell.MockSurfaceControlHelper;
 import com.android.wm.shell.ShellTestCase;
@@ -62,19 +63,18 @@
 
     @Mock
     private TaskInfo mTaskInfo;
-
     @Mock
     private PipAnimationController.PipAnimationCallback mPipAnimationCallback;
 
     @Before
     public void setUp() throws Exception {
-        mPipAnimationController = new PipAnimationController(
-                new PipSurfaceTransactionHelper());
+        MockitoAnnotations.initMocks(this);
+        mPipAnimationController = new PipAnimationController(new PipSurfaceTransactionHelper(
+                InstrumentationRegistry.getInstrumentation().getTargetContext()));
         mLeash = new SurfaceControl.Builder()
                 .setContainerLayer()
                 .setName("FakeLeash")
                 .build();
-        MockitoAnnotations.initMocks(this);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index b351f8f..857f578 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -42,6 +42,7 @@
 import android.util.Rational;
 import android.util.Size;
 import android.view.DisplayInfo;
+import android.view.SurfaceControl;
 import android.window.WindowContainerToken;
 
 import com.android.wm.shell.MockSurfaceControlHelper;
@@ -150,7 +151,7 @@
         final Rational aspectRatio = new Rational(2, 1);
 
         mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
-                createPipParams(aspectRatio)), null /* leash */);
+                createPipParams(aspectRatio)), mock(SurfaceControl.class));
 
         assertEquals(aspectRatio.floatValue(), mPipBoundsState.getAspectRatio(), 0.01f);
     }
@@ -158,7 +159,7 @@
     @Test
     public void onTaskAppeared_updatesLastPipComponentName() {
         mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1, createPipParams(null)),
-                null /* leash */);
+                mock(SurfaceControl.class));
 
         assertEquals(mComponent1, mPipBoundsState.getLastPipComponentName());
     }
@@ -169,7 +170,7 @@
 
         mSpiedPipTaskOrganizer.onTaskAppeared(
                 createTaskInfo(mComponent1, createPipParams(null), minSize),
-                null /* leash */);
+                mock(SurfaceControl.class));
 
         assertEquals(minSize, mPipBoundsState.getOverrideMinSize());
     }
@@ -179,7 +180,7 @@
         final Rational startAspectRatio = new Rational(2, 1);
         final Rational newAspectRatio = new Rational(1, 2);
         mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
-                createPipParams(startAspectRatio)), null /* leash */);
+                createPipParams(startAspectRatio)), mock(SurfaceControl.class));
 
         // It is in entering transition, should defer onTaskInfoChanged callback in this case.
         mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1,
@@ -197,7 +198,7 @@
         final Rational startAspectRatio = new Rational(2, 1);
         final Rational newAspectRatio = new Rational(1, 2);
         mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
-                createPipParams(startAspectRatio)), null /* leash */);
+                createPipParams(startAspectRatio)), mock(SurfaceControl.class));
         mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
 
         mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent1,
@@ -210,7 +211,7 @@
     @Test
     public void onTaskInfoChanged_inPip_updatesLastPipComponentName() {
         mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
-                createPipParams(null)), null /* leash */);
+                createPipParams(null)), mock(SurfaceControl.class));
         mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
 
         mSpiedPipTaskOrganizer.onTaskInfoChanged(createTaskInfo(mComponent2,
@@ -222,7 +223,7 @@
     @Test
     public void onTaskInfoChanged_inPip_updatesOverrideMinSize() {
         mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
-                createPipParams(null)), null /* leash */);
+                createPipParams(null)), mock(SurfaceControl.class));
         mSpiedPipTaskOrganizer.sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
 
         final Size minSize = new Size(400, 320);
@@ -235,7 +236,7 @@
     @Test
     public void onTaskVanished_clearsPipBounds() {
         mSpiedPipTaskOrganizer.onTaskAppeared(createTaskInfo(mComponent1,
-                createPipParams(null)), null /* leash */);
+                createPipParams(null)), mock(SurfaceControl.class));
         mPipBoundsState.setBounds(new Rect(100, 100, 200, 150));
 
         mSpiedPipTaskOrganizer.onTaskVanished(createTaskInfo(mComponent1, createPipParams(null)));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 304ca66..1d038f4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -182,7 +182,7 @@
 
         IBinder transition = mSplitScreenTransitions.startEnterTransition(
                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
-                new RemoteTransition(testRemote), mStageCoordinator);
+                new RemoteTransition(testRemote), mStageCoordinator, null);
         mMainStage.onTaskAppeared(mMainChild, createMockSurface());
         mSideStage.onTaskAppeared(mSideChild, createMockSurface());
         boolean accepted = mStageCoordinator.startAnimation(transition, info,
@@ -422,7 +422,7 @@
         TransitionInfo enterInfo = createEnterPairInfo();
         IBinder enterTransit = mSplitScreenTransitions.startEnterTransition(
                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
-                new RemoteTransition(new TestRemoteTransition()), mStageCoordinator);
+                new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null);
         mMainStage.onTaskAppeared(mMainChild, createMockSurface());
         mSideStage.onTaskAppeared(mSideChild, createMockSurface());
         mStageCoordinator.startAnimation(enterTransit, enterInfo,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 680034b..d1b837e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -21,23 +21,32 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.same;
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
 import android.content.Context;
 import android.graphics.Color;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
+import android.util.DisplayMetrics;
 import android.view.Display;
+import android.view.InsetsState;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
 import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManager.LayoutParams;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
@@ -53,6 +62,8 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Supplier;
 
 /**
@@ -66,6 +77,8 @@
 public class WindowDecorationTests extends ShellTestCase {
     private static final int CAPTION_HEIGHT_DP = 32;
     private static final int SHADOW_RADIUS_DP = 5;
+    private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400);
+    private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60);
 
     private final Rect mOutsetsDp = new Rect();
     private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult =
@@ -84,12 +97,11 @@
     @Mock
     private WindowContainerTransaction mMockWindowContainerTransaction;
 
-    private SurfaceControl.Builder mMockSurfaceControlBuilder;
+    private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>();
     private SurfaceControl.Transaction mMockSurfaceControlTransaction;
 
     @Before
     public void setUp() {
-        mMockSurfaceControlBuilder = createMockSurfaceControlBuilder(mock(SurfaceControl.class));
         mMockSurfaceControlTransaction = createMockSurfaceControlTransaction();
 
         doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
@@ -97,6 +109,128 @@
     }
 
     @Test
+    public void testLayoutResultCalculation_invisibleTask() {
+        final Display defaultDisplay = mock(Display.class);
+        doReturn(defaultDisplay).when(mMockDisplayController)
+                .getDisplay(Display.DEFAULT_DISPLAY);
+
+        final SurfaceControl decorContainerSurface = mock(SurfaceControl.class);
+        final SurfaceControl.Builder decorContainerSurfaceBuilder =
+                createMockSurfaceControlBuilder(decorContainerSurface);
+        mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
+        final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
+        final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
+                createMockSurfaceControlBuilder(taskBackgroundSurface);
+        mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
+
+        final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+                new ActivityManager.TaskDescription.Builder()
+                        .setBackgroundColor(Color.YELLOW);
+        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setDisplayId(Display.DEFAULT_DISPLAY)
+                .setTaskDescriptionBuilder(taskDescriptionBuilder)
+                .setBounds(TASK_BOUNDS)
+                .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+                .setVisible(false)
+                .build();
+        taskInfo.isFocused = false;
+        // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
+        // 64px.
+        taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+        mOutsetsDp.set(10, 20, 30, 40);
+
+        final SurfaceControl taskSurface = mock(SurfaceControl.class);
+        final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+
+        windowDecor.relayout(taskInfo);
+
+        verify(decorContainerSurfaceBuilder, never()).build();
+        verify(taskBackgroundSurfaceBuilder, never()).build();
+        verify(mMockSurfaceControlViewHostFactory, never())
+                .create(any(), any(), any(), anyBoolean());
+
+        verify(mMockSurfaceControlTransaction).hide(taskSurface);
+
+        assertNull(mRelayoutResult.mRootView);
+    }
+
+    @Test
+    public void testLayoutResultCalculation_visibleFocusedTask() {
+        final Display defaultDisplay = mock(Display.class);
+        doReturn(defaultDisplay).when(mMockDisplayController)
+                .getDisplay(Display.DEFAULT_DISPLAY);
+
+        final SurfaceControl decorContainerSurface = mock(SurfaceControl.class);
+        final SurfaceControl.Builder decorContainerSurfaceBuilder =
+                createMockSurfaceControlBuilder(decorContainerSurface);
+        mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
+        final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
+        final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
+                createMockSurfaceControlBuilder(taskBackgroundSurface);
+        mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
+
+        final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+                new ActivityManager.TaskDescription.Builder()
+                        .setBackgroundColor(Color.YELLOW);
+        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setDisplayId(Display.DEFAULT_DISPLAY)
+                .setTaskDescriptionBuilder(taskDescriptionBuilder)
+                .setBounds(TASK_BOUNDS)
+                .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+                .setVisible(true)
+                .build();
+        taskInfo.isFocused = true;
+        // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
+        // 64px.
+        taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+        mOutsetsDp.set(10, 20, 30, 40);
+
+        final SurfaceControl taskSurface = mock(SurfaceControl.class);
+        final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+
+        windowDecor.relayout(taskInfo);
+
+        verify(decorContainerSurfaceBuilder).setParent(taskSurface);
+        verify(decorContainerSurfaceBuilder).setContainerLayer();
+        verify(mMockSurfaceControlTransaction).setTrustedOverlay(decorContainerSurface, true);
+        verify(mMockSurfaceControlTransaction).setPosition(decorContainerSurface, -20, -40);
+        verify(mMockSurfaceControlTransaction).setWindowCrop(decorContainerSurface, 380, 220);
+
+        verify(taskBackgroundSurfaceBuilder).setParent(taskSurface);
+        verify(taskBackgroundSurfaceBuilder).setEffectLayer();
+        verify(mMockSurfaceControlTransaction).setWindowCrop(taskBackgroundSurface, 300, 100);
+        verify(mMockSurfaceControlTransaction)
+                .setColor(taskBackgroundSurface, new float[] {1.f, 1.f, 0.f});
+        verify(mMockSurfaceControlTransaction).setShadowRadius(taskBackgroundSurface, 10);
+
+        verify(mMockSurfaceControlViewHostFactory)
+                .create(any(), eq(defaultDisplay), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHost)
+                .setView(same(mMockView),
+                        argThat(lp -> lp.height == 64
+                                && lp.width == 300
+                                && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
+        if (ViewRootImpl.CAPTION_ON_SHELL) {
+            verify(mMockView).setTaskFocusState(true);
+            verify(mMockWindowContainerTransaction)
+                    .addRectInsetsProvider(taskInfo.token,
+                            new Rect(100, 300, 400, 364),
+                            new int[] { InsetsState.ITYPE_CAPTION_BAR });
+        }
+
+        verify(mMockSurfaceControlTransaction)
+                .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y);
+        verify(mMockSurfaceControlTransaction)
+                .setCrop(taskSurface, new Rect(-20, -40, 360, 180));
+        verify(mMockSurfaceControlTransaction)
+                .show(taskSurface);
+
+        assertEquals(380, mRelayoutResult.mWidth);
+        assertEquals(220, mRelayoutResult.mHeight);
+        assertEquals(2, mRelayoutResult.mDensity, 0.f);
+    }
+
+    @Test
     public void testNotCrashWhenDisplayAppearsAfterTask() {
         doReturn(mock(Display.class)).when(mMockDisplayController)
                 .getDisplay(Display.DEFAULT_DISPLAY);
@@ -145,10 +279,24 @@
     private TestWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
         return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
-                taskInfo, testSurface, () -> mMockSurfaceControlBuilder,
+                taskInfo, testSurface, new MockSurfaceControlBuilderSupplier(),
                 mMockSurfaceControlViewHostFactory);
     }
 
+    private class MockSurfaceControlBuilderSupplier implements Supplier<SurfaceControl.Builder> {
+        private int mNumOfCalls = 0;
+
+        @Override
+        public SurfaceControl.Builder get() {
+            final SurfaceControl.Builder builder =
+                    mNumOfCalls < mMockSurfaceControlBuilders.size()
+                            ? mMockSurfaceControlBuilders.get(mNumOfCalls)
+                            : createMockSurfaceControlBuilder(mock(SurfaceControl.class));
+            ++mNumOfCalls;
+            return builder;
+        }
+    }
+
     private static class TestView extends View implements TaskFocusStateConsumer {
         private TestView(Context context) {
             super(context);
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
index d2a4de4..b9a6ecc 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
@@ -20,6 +20,11 @@
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
     android:background="?android:attr/colorBackground"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingTop="@dimen/settingslib_switchbar_margin"
+    android:paddingBottom="@dimen/settingslib_switchbar_margin"
     android:orientation="vertical">
 
     <LinearLayout
@@ -27,7 +32,6 @@
         android:minHeight="@dimen/settingslib_min_switch_bar_height"
         android:layout_height="wrap_content"
         android:layout_width="match_parent"
-        android:layout_margin="@dimen/settingslib_switchbar_margin"
         android:paddingStart="@dimen/settingslib_switchbar_padding_left"
         android:paddingEnd="@dimen/settingslib_switchbar_padding_right">
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 58944f6..4714ff9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -576,6 +576,15 @@
             return;
         }
 
+        // The profiles list's sequence will affect the bluetooth icon at
+        // BluetoothUtils.getBtClassDrawableWithDescription(Context,CachedBluetoothDevice).
+
+        // Moving the LE audio profile to be the first priority if the device supports LE audio.
+        if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO) && mLeAudioProfile != null) {
+            profiles.add(mLeAudioProfile);
+            removedProfiles.remove(mLeAudioProfile);
+        }
+
         if (mHeadsetProfile != null) {
             if ((ArrayUtils.contains(localUuids, BluetoothUuid.HSP_AG)
                     && ArrayUtils.contains(uuids, BluetoothUuid.HSP))
@@ -660,11 +669,6 @@
             removedProfiles.remove(mHearingAidProfile);
         }
 
-        if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO) && mLeAudioProfile != null) {
-            profiles.add(mLeAudioProfile);
-            removedProfiles.remove(mLeAudioProfile);
-        }
-
         if (mSapProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.SAP)) {
             profiles.add(mSapProfile);
             removedProfiles.remove(mSapProfile);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index 281501e..f4355c3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -84,7 +84,8 @@
     private InfoMediaManager mInfoMediaManager;
     private String mPackageName;
     private MediaDevice mOnTransferBluetoothDevice;
-    private AudioManager mAudioManager;
+    @VisibleForTesting
+    AudioManager mAudioManager;
 
     @VisibleForTesting
     List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
index 4332bd2..45c0d78 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -57,6 +57,7 @@
     private final Handler mHandler;
     private final Handler mMainThreadHandler;
     private final Set<Integer> mNetworks = new HashSet<>();
+    private int mPrimaryNetworkId;
     // Save the previous HISTORY_SIZE states for logging.
     private final String[] mHistory = new String[HISTORY_SIZE];
     // Where to copy the next state into.
@@ -106,6 +107,7 @@
             if (!mNetworks.contains(network.getNetId())) {
                 mNetworks.add(network.getNetId());
             }
+            mPrimaryNetworkId = network.getNetId();
             updateWifiInfo(wifiInfo);
             updateStatusLabel();
             mMainThreadHandler.post(() -> postResults());
@@ -121,10 +123,13 @@
             recordLastWifiNetwork(log);
             if (mNetworks.contains(network.getNetId())) {
                 mNetworks.remove(network.getNetId());
-                updateWifiInfo(null);
-                updateStatusLabel();
-                mMainThreadHandler.post(() -> postResults());
             }
+            if (network.getNetId() != mPrimaryNetworkId) {
+                return;
+            }
+            updateWifiInfo(null);
+            updateStatusLabel();
+            mMainThreadHandler.post(() -> postResults());
         }
     };
     private final NetworkCallback mDefaultNetworkCallback =
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index 8e850b2..24bb1bc 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.media;
 
-import static android.bluetooth.BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES;
 import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -29,10 +28,12 @@
 import static org.mockito.Mockito.when;
 
 import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
+import android.media.AudioSystem;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
 import android.media.RoutingSessionInfo;
@@ -71,6 +72,7 @@
     private static final String TEST_CURRENT_DEVICE_ID = "currentDevice_id";
     private static final String TEST_PACKAGE_NAME = "com.test.playmusic";
     private static final String TEST_SESSION_ID = "session_id";
+    private static final String TEST_ADDRESS = "00:01:02:03:04:05";
 
     @Mock
     private InfoMediaManager mInfoMediaManager;
@@ -90,6 +92,8 @@
     private MediaRoute2Info mRouteInfo1;
     @Mock
     private MediaRoute2Info mRouteInfo2;
+    @Mock
+    private AudioManager mAudioManager;
 
     private Context mContext;
     private LocalMediaManager mLocalMediaManager;
@@ -118,6 +122,7 @@
                 TEST_PACKAGE_NAME);
         mLocalMediaManager = new LocalMediaManager(mContext, mLocalBluetoothManager,
                 mInfoMediaManager, "com.test.packagename");
+        mLocalMediaManager.mAudioManager = mAudioManager;
     }
 
     @Test
@@ -551,16 +556,12 @@
     }
 
     @Test
-    public void onDeviceListAdded_haveDisconnectedDevice_addDisconnectedDevice() {
+    public void onDeviceListAdded_haveMutingExpectedDevice_addMutingExpectedDevice() {
         final List<MediaDevice> devices = new ArrayList<>();
         final MediaDevice device1 = mock(MediaDevice.class);
-        final MediaDevice device2 = mock(MediaDevice.class);
-        final MediaDevice device3 = mock(MediaDevice.class);
         mLocalMediaManager.mPhoneDevice = mock(PhoneMediaDevice.class);
         devices.add(device1);
-        devices.add(device2);
-        mLocalMediaManager.mMediaDevices.add(device3);
-        mLocalMediaManager.mMediaDevices.add(mLocalMediaManager.mPhoneDevice);
+        devices.add(mLocalMediaManager.mPhoneDevice);
 
         final List<LocalBluetoothProfile> profiles = new ArrayList<>();
         final A2dpProfile a2dpProfile = mock(A2dpProfile.class);
@@ -573,22 +574,25 @@
         bluetoothDevices.add(bluetoothDevice);
         mShadowBluetoothAdapter.setMostRecentlyConnectedDevices(bluetoothDevices);
 
+        AudioDeviceAttributes audioDeviceAttributes = new AudioDeviceAttributes(
+                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, TEST_ADDRESS);
+
+        when(mAudioManager.getMutingExpectedDevice()).thenReturn(audioDeviceAttributes);
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(cachedManager);
         when(cachedManager.findDevice(bluetoothDevice)).thenReturn(cachedDevice);
         when(cachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         when(cachedDevice.isConnected()).thenReturn(false);
         when(cachedDevice.getConnectableProfiles()).thenReturn(profiles);
         when(cachedDevice.getDevice()).thenReturn(bluetoothDevice);
+        when(cachedDevice.getAddress()).thenReturn(TEST_ADDRESS);
         when(mA2dpProfile.getActiveDevice()).thenReturn(bluetoothDevice);
         when(mHapProfile.getActiveDevices()).thenReturn(new ArrayList<>());
 
         when(device1.getId()).thenReturn(TEST_DEVICE_ID_1);
-        when(device2.getId()).thenReturn(TEST_DEVICE_ID_2);
-        when(device3.getId()).thenReturn(TEST_DEVICE_ID_3);
         when(device1.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE);
         when(mLocalMediaManager.mPhoneDevice.getId()).thenReturn("test_phone_id");
 
-        assertThat(mLocalMediaManager.mMediaDevices).hasSize(2);
+        assertThat(mLocalMediaManager.mMediaDevices).hasSize(0);
         mLocalMediaManager.registerCallback(mCallback);
         mLocalMediaManager.mMediaDeviceCallback.onDeviceListAdded(devices);
 
@@ -634,68 +638,6 @@
     }
 
     @Test
-    public void onDeviceListAdded_haveDisconnectedDevice_list5DisconnectedDevice() {
-        final List<MediaDevice> devices = new ArrayList<>();
-        final MediaDevice device1 = mock(MediaDevice.class);
-        final MediaDevice device2 = mock(MediaDevice.class);
-        final MediaDevice device3 = mock(MediaDevice.class);
-        mLocalMediaManager.mPhoneDevice = mock(PhoneMediaDevice.class);
-        devices.add(device1);
-        devices.add(device2);
-        mLocalMediaManager.mMediaDevices.add(device3);
-        mLocalMediaManager.mMediaDevices.add(mLocalMediaManager.mPhoneDevice);
-
-        final List<LocalBluetoothProfile> profiles = new ArrayList<>();
-        final A2dpProfile a2dpProfile = mock(A2dpProfile.class);
-        profiles.add(a2dpProfile);
-
-        final List<BluetoothDevice> bluetoothDevices = new ArrayList<>();
-        final BluetoothDevice bluetoothDevice = mock(BluetoothDevice.class);
-        final BluetoothDevice bluetoothDevice2 = mock(BluetoothDevice.class);
-        final BluetoothDevice bluetoothDevice3 = mock(BluetoothDevice.class);
-        final BluetoothDevice bluetoothDevice4 = mock(BluetoothDevice.class);
-        final BluetoothDevice bluetoothDevice5 = mock(BluetoothDevice.class);
-        final BluetoothDevice bluetoothDevice6 = mock(BluetoothDevice.class);
-        final BluetoothClass bluetoothClass = mock(BluetoothClass.class);
-        final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
-        final CachedBluetoothDeviceManager cachedManager = mock(CachedBluetoothDeviceManager.class);
-        bluetoothDevices.add(bluetoothDevice);
-        bluetoothDevices.add(bluetoothDevice2);
-        bluetoothDevices.add(bluetoothDevice3);
-        bluetoothDevices.add(bluetoothDevice4);
-        bluetoothDevices.add(bluetoothDevice5);
-        bluetoothDevices.add(bluetoothDevice6);
-        mShadowBluetoothAdapter.setMostRecentlyConnectedDevices(bluetoothDevices);
-
-        when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(cachedManager);
-        when(cachedManager.findDevice(bluetoothDevice)).thenReturn(cachedDevice);
-        when(cachedManager.findDevice(bluetoothDevice2)).thenReturn(cachedDevice);
-        when(cachedManager.findDevice(bluetoothDevice3)).thenReturn(cachedDevice);
-        when(cachedManager.findDevice(bluetoothDevice4)).thenReturn(cachedDevice);
-        when(cachedManager.findDevice(bluetoothDevice5)).thenReturn(cachedDevice);
-        when(cachedManager.findDevice(bluetoothDevice6)).thenReturn(cachedDevice);
-        when(cachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        when(cachedDevice.isConnected()).thenReturn(false);
-        when(cachedDevice.getDevice()).thenReturn(bluetoothDevice);
-        when(cachedDevice.getConnectableProfiles()).thenReturn(profiles);
-        when(bluetoothDevice.getBluetoothClass()).thenReturn(bluetoothClass);
-        when(bluetoothClass.getDeviceClass()).thenReturn(AUDIO_VIDEO_HEADPHONES);
-
-        when(device1.getId()).thenReturn(TEST_DEVICE_ID_1);
-        when(device2.getId()).thenReturn(TEST_DEVICE_ID_2);
-        when(device3.getId()).thenReturn(TEST_DEVICE_ID_3);
-        when(device1.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE);
-        when(mLocalMediaManager.mPhoneDevice.getId()).thenReturn("test_phone_id");
-
-        assertThat(mLocalMediaManager.mMediaDevices).hasSize(2);
-        mLocalMediaManager.registerCallback(mCallback);
-        mLocalMediaManager.mMediaDeviceCallback.onDeviceListAdded(devices);
-
-        assertThat(mLocalMediaManager.mMediaDevices).hasSize(7);
-        verify(mCallback).onDeviceListUpdate(any());
-    }
-
-    @Test
     public void onDeviceListAdded_bluetoothAdapterIsNull_noDisconnectedDeviceAdded() {
         final List<MediaDevice> devices = new ArrayList<>();
         final MediaDevice device1 = mock(MediaDevice.class);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java
new file mode 100644
index 0000000..dc7e313d
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.settingslib.wifi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkScoreManager;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class WifiStatusTrackerTest {
+    @Mock Context mContext;
+    @Mock WifiManager mWifiManager;
+    @Mock NetworkScoreManager mNetworkScoreManager;
+    @Mock ConnectivityManager mConnectivityManager;
+    @Mock Runnable mCallback;
+
+    private final ArgumentCaptor<ConnectivityManager.NetworkCallback>
+            mNetworkCallbackCaptor =
+            ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    /**
+     * Verify that we only clear the WifiInfo if the primary network was lost.
+     */
+    @Test
+    public void testWifiInfoClearedOnPrimaryNetworkLost() {
+        WifiStatusTracker wifiStatusTracker = new WifiStatusTracker(mContext, mWifiManager,
+                mNetworkScoreManager, mConnectivityManager, mCallback);
+        wifiStatusTracker.setListening(true);
+
+        verify(mConnectivityManager)
+                .registerNetworkCallback(any(), mNetworkCallbackCaptor.capture(), any());
+
+        // Trigger a validation callback for the primary Wifi network.
+        WifiInfo primaryWifiInfo = Mockito.mock(WifiInfo.class);
+        when(primaryWifiInfo.makeCopy(anyLong())).thenReturn(primaryWifiInfo);
+        when(primaryWifiInfo.isPrimary()).thenReturn(true);
+        int primaryRssi = -55;
+        when(primaryWifiInfo.getRssi()).thenReturn(primaryRssi);
+        NetworkCapabilities primaryCap = new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .setTransportInfo(primaryWifiInfo)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                .build();
+        Network primaryNetwork = Mockito.mock(Network.class);
+        int primaryNetworkId = 1;
+        when(primaryNetwork.getNetId()).thenReturn(primaryNetworkId);
+        mNetworkCallbackCaptor.getValue().onCapabilitiesChanged(primaryNetwork, primaryCap);
+
+        // Verify primary wifi info is the one being used.
+        assertThat(wifiStatusTracker.connected).isTrue();
+        assertThat(wifiStatusTracker.rssi).isEqualTo(primaryRssi);
+
+        // Trigger a validation callback for the non-primary Wifi network.
+        WifiInfo nonPrimaryWifiInfo = Mockito.mock(WifiInfo.class);
+        when(nonPrimaryWifiInfo.makeCopy(anyLong())).thenReturn(nonPrimaryWifiInfo);
+        when(nonPrimaryWifiInfo.isPrimary()).thenReturn(false);
+        int nonPrimaryRssi = -75;
+        when(primaryWifiInfo.getRssi()).thenReturn(nonPrimaryRssi);
+        NetworkCapabilities nonPrimaryCap = new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .setTransportInfo(nonPrimaryWifiInfo)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                .build();
+        Network nonPrimaryNetwork = Mockito.mock(Network.class);
+        int nonPrimaryNetworkId = 2;
+        when(nonPrimaryNetwork.getNetId()).thenReturn(nonPrimaryNetworkId);
+        mNetworkCallbackCaptor.getValue().onCapabilitiesChanged(nonPrimaryNetwork, nonPrimaryCap);
+
+        // Verify primary wifi info is still the one being used.
+        assertThat(wifiStatusTracker.connected).isTrue();
+        assertThat(wifiStatusTracker.rssi).isEqualTo(primaryRssi);
+
+        // Lose the non-primary network.
+        mNetworkCallbackCaptor.getValue().onLost(nonPrimaryNetwork);
+
+        // Verify primary wifi info is still the one being used.
+        assertThat(wifiStatusTracker.connected).isTrue();
+        assertThat(wifiStatusTracker.rssi).isEqualTo(primaryRssi);
+
+        // Lose the primary network.
+        mNetworkCallbackCaptor.getValue().onLost(primaryNetwork);
+
+        // Verify we aren't connected anymore.
+        assertThat(wifiStatusTracker.connected).isFalse();
+    }
+}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index ce33160..d1f10a6 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -118,6 +118,10 @@
         Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD,
         Settings.Secure.FACE_UNLOCK_APP_ENABLED,
         Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION,
+        Settings.Secure.FINGERPRINT_SIDE_FPS_KG_POWER_WINDOW,
+        Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW,
+        Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
+        Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME,
         Settings.Secure.ACTIVE_UNLOCK_ON_WAKE,
         Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT,
         Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index e82bf04..fbbdd32 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -51,6 +51,7 @@
                                 || (val == BatteryManager.BATTERY_PLUGGED_AC)
                                 || (val == BatteryManager.BATTERY_PLUGGED_USB)
                                 || (val == BatteryManager.BATTERY_PLUGGED_WIRELESS)
+                                || (val == BatteryManager.BATTERY_PLUGGED_DOCK)
                                 || (val
                                         == (BatteryManager.BATTERY_PLUGGED_AC
                                                 | BatteryManager.BATTERY_PLUGGED_USB))
@@ -58,12 +59,29 @@
                                         == (BatteryManager.BATTERY_PLUGGED_AC
                                                 | BatteryManager.BATTERY_PLUGGED_WIRELESS))
                                 || (val
+                                        == (BatteryManager.BATTERY_PLUGGED_AC
+                                                | BatteryManager.BATTERY_PLUGGED_DOCK))
+                                || (val
                                         == (BatteryManager.BATTERY_PLUGGED_USB
                                                 | BatteryManager.BATTERY_PLUGGED_WIRELESS))
                                 || (val
+                                        == (BatteryManager.BATTERY_PLUGGED_USB
+                                                | BatteryManager.BATTERY_PLUGGED_DOCK))
+                                || (val
+                                        == (BatteryManager.BATTERY_PLUGGED_WIRELESS
+                                                | BatteryManager.BATTERY_PLUGGED_DOCK))
+                                || (val
+                                        == (BatteryManager.BATTERY_PLUGGED_AC
+                                                | BatteryManager.BATTERY_PLUGGED_USB
+                                                | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+                                || (val
                                         == (BatteryManager.BATTERY_PLUGGED_AC
                                                 | BatteryManager.BATTERY_PLUGGED_USB
-                                                | BatteryManager.BATTERY_PLUGGED_WIRELESS));
+                                                | BatteryManager.BATTERY_PLUGGED_DOCK))
+                                || (val
+                                        == (BatteryManager.BATTERY_PLUGGED_USB
+                                                | BatteryManager.BATTERY_PLUGGED_WIRELESS
+                                                | BatteryManager.BATTERY_PLUGGED_DOCK));
                     } catch (NumberFormatException e) {
                         return false;
                     }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 5d77378..4aadf72 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -172,6 +172,11 @@
                 Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_KEYGUARD_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_DISMISSES_KEYGUARD, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_KG_POWER_WINDOW, NON_NEGATIVE_INTEGER_VALIDATOR);
+        VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW, NON_NEGATIVE_INTEGER_VALIDATOR);
+        VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
+                NON_NEGATIVE_INTEGER_VALIDATOR);
+        VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index ccfeae4..aa3a983 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2358,6 +2358,12 @@
                 SecureSettingsProto.PowerMenuPrivacy.SHOW);
         p.end(powerMenuPrivacyToken);
 
+        final long extraLowPowerModeToken = p.start(SecureSettingsProto.EXTRA_LOW_POWER_MODE);
+        dumpSetting(s, p,
+                Settings.Secure.EXTRA_AUTOMATIC_POWER_SAVE_MODE,
+                SecureSettingsProto.ExtraLowPowerMode.EXTRA_AUTOMATIC_POWER_SAVE_MODE);
+        p.end(extraLowPowerModeToken);
+
         final long printServiceToken = p.start(SecureSettingsProto.PRINT_SERVICE);
         dumpSetting(s, p,
                 Settings.Secure.PRINT_SERVICE_SEARCH_URI,
diff --git a/packages/SystemUI/src/com/android/keyguard/FontInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
similarity index 98%
rename from packages/SystemUI/src/com/android/keyguard/FontInterpolator.kt
rename to packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
index 962c002..3d341af 100644
--- a/packages/SystemUI/src/com/android/keyguard/FontInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.keyguard
+package com.android.systemui.animation
 
 import android.graphics.fonts.Font
 import android.graphics.fonts.FontVariationAxis
@@ -61,7 +61,7 @@
         var index: Int,
         val sortedAxes: MutableList<FontVariationAxis>
     ) {
-        constructor(font: Font, axes: List<FontVariationAxis>):
+        constructor(font: Font, axes: List<FontVariationAxis>) :
                 this(font.sourceIdentifier,
                         font.ttcIndex,
                         axes.toMutableList().apply { sortBy { it.tag } }
diff --git a/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/keyguard/TextAnimator.kt
rename to packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index ade89af..f79b328 100644
--- a/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.keyguard
+package com.android.systemui.animation
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
@@ -61,8 +61,8 @@
     private val invalidateCallback: () -> Unit
 ) {
     // Following two members are for mutable for testing purposes.
-    internal var textInterpolator: TextInterpolator = TextInterpolator(layout)
-    internal var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply {
+    public var textInterpolator: TextInterpolator = TextInterpolator(layout)
+    public var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply {
         duration = DEFAULT_ANIMATION_DURATION
         addUpdateListener {
             textInterpolator.progress = it.animatedValue as Float
@@ -279,4 +279,4 @@
         put(key, v)
     }
     return v
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
similarity index 99%
rename from packages/SystemUI/src/com/android/keyguard/TextInterpolator.kt
rename to packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
index 20dbe29..ff64c78 100644
--- a/packages/SystemUI/src/com/android/keyguard/TextInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.keyguard
+package com.android.systemui.animation
 
 import android.graphics.Canvas
 import android.graphics.Paint
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
new file mode 100644
index 0000000..0c191607
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.plugins
+
+import android.graphics.drawable.Drawable
+import android.view.View
+import com.android.internal.colorextraction.ColorExtractor
+import com.android.systemui.plugins.annotations.ProvidesInterface
+import java.io.PrintWriter
+import java.util.Locale
+import java.util.TimeZone
+
+/** Identifies a clock design */
+typealias ClockId = String
+
+/** A Plugin which exposes the ClockProvider interface */
+@ProvidesInterface(action = ClockProviderPlugin.ACTION, version = ClockProviderPlugin.VERSION)
+interface ClockProviderPlugin : Plugin, ClockProvider {
+    companion object {
+        const val ACTION = "com.android.systemui.action.PLUGIN_CLOCK_PROVIDER"
+        const val VERSION = 1
+    }
+}
+
+/** Interface for building clocks and providing information about those clocks */
+interface ClockProvider {
+    /** Returns metadata for all clocks this provider knows about */
+    fun getClocks(): List<ClockMetadata>
+
+    /** Initializes and returns the target clock design */
+    fun createClock(id: ClockId): Clock
+
+    /** A static thumbnail for rendering in some examples */
+    fun getClockThumbnail(id: ClockId): Drawable?
+}
+
+/** Interface for controlling an active clock */
+interface Clock {
+    /** A small version of the clock, appropriate for smaller viewports */
+    val smallClock: View
+
+    /** A large version of the clock, appropriate when a bigger viewport is available */
+    val largeClock: View
+
+    /** Events that clocks may need to respond to */
+    val events: ClockEvents
+
+    /** Triggers for various animations */
+    val animation: ClockAnimation
+
+    /** Optional method for dumping debug information */
+    fun dump(pw: PrintWriter) { }
+}
+
+/** Events that should call when various rendering parameters change */
+interface ClockEvents {
+    /** Call every time tick */
+    fun onTimeTick()
+
+    /** Call whenever timezone changes */
+    fun onTimeZoneChanged(timeZone: TimeZone) { }
+
+    /** Call whenever the text time format changes (12hr vs 24hr) */
+    fun onTimeFormatChanged(is24Hr: Boolean) { }
+
+    /** Call whenever the locale changes */
+    fun onLocaleChanged(locale: Locale) { }
+
+    /** Call whenever font settings change */
+    fun onFontSettingChanged() { }
+
+    /** Call whenever the color pallete should update */
+    fun onColorPaletteChanged(palette: ColorExtractor.GradientColors) { }
+}
+
+/** Methods which trigger various clock animations */
+interface ClockAnimation {
+    /** Initializes the doze & fold animation positions. Defaults to neither folded nor dozing. */
+    fun initialize(dozeFraction: Float, foldFraction: Float) { }
+
+    /** Runs an enter animation (if any) */
+    fun enter() { }
+
+    /** Sets how far into AOD the device currently is. */
+    fun doze(fraction: Float) { }
+
+    /** Sets how far into the folding animation the device is. */
+    fun fold(fraction: Float) { }
+
+    /** Runs the battery animation (if any). */
+    fun charge() { }
+}
+
+/** Some data about a clock design */
+data class ClockMetadata(
+    val clockId: ClockId,
+    val name: String
+)
diff --git a/packages/SystemUI/res-keyguard/font/clock.xml b/packages/SystemUI/res-keyguard/font/clock.xml
index d0867e9..0137dc3 100644
--- a/packages/SystemUI/res-keyguard/font/clock.xml
+++ b/packages/SystemUI/res-keyguard/font/clock.xml
@@ -22,6 +22,7 @@
 ** Should include all numeric glyphs in all supported locales.
 ** Recommended: font with variable width to support AOD => LS animations
 -->
+<!-- TODO: Remove when clock migration complete -->
 <font-family xmlns:android="http://schemas.android.com/apk/res/android">
     <font android:typeface="monospace"/>
 </font-family>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index 87a9825..6a38507 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -31,7 +31,7 @@
         android:layout_alignParentStart="true"
         android:layout_alignParentTop="true"
         android:paddingStart="@dimen/clock_padding_start">
-        <com.android.keyguard.AnimatableClockView
+        <com.android.systemui.shared.clocks.AnimatableClockView
             android:id="@+id/animatable_clock_view"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
@@ -53,7 +53,7 @@
         android:layout_height="wrap_content"
         android:layout_below="@id/keyguard_slice_view"
         android:visibility="gone">
-        <com.android.keyguard.AnimatableClockView
+        <com.android.systemui.shared.clocks.AnimatableClockView
             android:id="@+id/animatable_clock_view_large"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res-keyguard/values/attrs.xml b/packages/SystemUI/res-keyguard/values/attrs.xml
index 25be37a..f2a1e26 100644
--- a/packages/SystemUI/res-keyguard/values/attrs.xml
+++ b/packages/SystemUI/res-keyguard/values/attrs.xml
@@ -41,10 +41,4 @@
     <attr name="passwordStyle" format="reference" />
 
     <attr name="numPadKeyStyle" format="reference" />
-
-    <declare-styleable name="AnimatableClockView">
-        <attr name="dozeWeight" format="integer" />
-        <attr name="lockScreenWeight" format="integer" />
-        <attr name="chargeAnimationDelay" format="integer" />
-    </declare-styleable>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/donottranslate.xml b/packages/SystemUI/res-keyguard/values/donottranslate.xml
index e677797..f9872f8 100644
--- a/packages/SystemUI/res-keyguard/values/donottranslate.xml
+++ b/packages/SystemUI/res-keyguard/values/donottranslate.xml
@@ -23,11 +23,5 @@
     <!-- Skeleton string format for displaying the date shorter. -->
     <string name="abbrev_month_day_no_year">MMMd</string>
 
-    <!-- Skeleton string format for displaying the time in 12-hour format. -->
-    <string name="clock_12hr_format">hm</string>
-
-    <!-- Skeleton string format for displaying the time in 24-hour format. -->
-    <string name="clock_24hr_format">Hm</string>
-
     <string name="num_pad_key_ratio">1</string>
 </resources>
diff --git a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
index e929169..a97c90c 100644
--- a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
+++ b/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
@@ -54,7 +54,6 @@
         android:layout_height="wrap_content"
         android:layout_gravity="center"
         android:background="@drawable/rounded_bg_full_large_radius"
-        android:onClick="dismissActivity"
         android:text="@string/got_it"
         android:textColor="?androidprv:attr/textColorOnAccent"
         android:layout_marginBottom="60dp"
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index cf49728..6f87169 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -44,7 +44,6 @@
         <item name="android:windowLightStatusBar">false</item>
         <item name="android:windowLightNavigationBar">false</item>
         <item name="android:navigationBarColor">?android:attr/colorBackgroundFloating</item>
-        <item name="android:textColorSecondary">?android:attr/textColorPrimaryInverse</item>
     </style>
 
     <style name="FloatingOverlay" parent="@android:style/Theme.DeviceDefault.DayNight">
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 0cafa35..3fb00a3 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -59,8 +59,8 @@
     <dimen name="navigation_edge_entry_margin">4dp</dimen>
     <dimen name="navigation_edge_entry_background_width">8dp</dimen>
     <dimen name="navigation_edge_entry_background_height">60dp</dimen>
-    <dimen name="navigation_edge_entry_edge_corners">6dp</dimen>
-    <dimen name="navigation_edge_entry_far_corners">6dp</dimen>
+    <dimen name="navigation_edge_entry_edge_corners">30dp</dimen>
+    <dimen name="navigation_edge_entry_far_corners">30dp</dimen>
     <dimen name="navigation_edge_entry_arrow_length">10dp</dimen>
     <dimen name="navigation_edge_entry_arrow_height">7dp</dimen>
 
@@ -90,6 +90,7 @@
 
     <dimen name="navigation_edge_cancelled_arrow_length">12dp</dimen>
     <dimen name="navigation_edge_cancelled_arrow_height">0dp</dimen>
+    <dimen name="navigation_edge_cancelled_edge_corners">6dp</dimen>
 
     <!-- Height of notification icons in the status bar -->
     <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen>
@@ -665,6 +666,7 @@
     <!-- With the large clock, move up slightly from the center -->
     <dimen name="keyguard_large_clock_top_margin">-60dp</dimen>
 
+    <!-- TODO: Remove during migration -->
     <!-- Default line spacing multiplier between hours and minutes of the keyguard clock -->
     <item name="keyguard_clock_line_spacing_scale" type="dimen" format="float">.7</item>
     <!-- Burmese line spacing multiplier between hours and minutes of the keyguard clock -->
@@ -889,6 +891,7 @@
     <dimen name="burn_in_prevention_offset_y_clock">42dp</dimen>
 
     <!-- Clock maximum font size (dp is intentional, to prevent any further scaling) -->
+     <!-- TODO: Remove when clock migration complete -->
     <dimen name="large_clock_text_size">150dp</dimen>
     <dimen name="clock_text_size">86dp</dimen>
 
@@ -898,7 +901,7 @@
     <!-- The maximum offset for the under-display fingerprint sensor (UDFPS) icon in either
          direction that elements are moved to prevent burn-in on AOD-->
     <dimen name="udfps_burn_in_offset_x">7px</dimen>
-    <dimen name="udfps_burn_in_offset_y">28px</dimen>
+    <dimen name="udfps_burn_in_offset_y">20px</dimen>
 
     <!-- The absolute side margins of quick settings -->
     <dimen name="quick_settings_bottom_margin_media">8dp</dimen>
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 114ea65..18bd6b4 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -47,6 +47,8 @@
     ],
     static_libs: [
         "PluginCoreLib",
+        "SystemUIAnimationLib",
+        "SystemUIPluginLib",
         "SystemUIUnfoldLib",
         "androidx.dynamicanimation_dynamicanimation",
         "androidx.concurrent_concurrent-futures",
@@ -54,6 +56,9 @@
         "dagger2",
         "jsr330",
     ],
+    resource_dirs: [
+        "res",
+    ],
     java_version: "1.8",
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
diff --git a/packages/SystemUI/shared/res/drawable/clock_default_thumbnail.xml b/packages/SystemUI/shared/res/drawable/clock_default_thumbnail.xml
new file mode 100644
index 0000000..be72d0b
--- /dev/null
+++ b/packages/SystemUI/shared/res/drawable/clock_default_thumbnail.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="#FFFF00FF" />
+</shape>
diff --git a/packages/SystemUI/shared/res/layout/clock_default_large.xml b/packages/SystemUI/shared/res/layout/clock_default_large.xml
new file mode 100644
index 0000000..8510a0a
--- /dev/null
+++ b/packages/SystemUI/shared/res/layout/clock_default_large.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<com.android.systemui.shared.clocks.AnimatableClockView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/animatable_clock_view_large"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:gravity="center_horizontal"
+    android:textSize="@dimen/large_clock_text_size"
+    android:fontFamily="@*android:string/config_clockFontFamily"
+    android:typeface="monospace"
+    android:elegantTextHeight="false"
+    chargeAnimationDelay="200"
+    dozeWeight="200"
+    lockScreenWeight="400" />
diff --git a/packages/SystemUI/shared/res/layout/clock_default_small.xml b/packages/SystemUI/shared/res/layout/clock_default_small.xml
new file mode 100644
index 0000000..ec0e427
--- /dev/null
+++ b/packages/SystemUI/shared/res/layout/clock_default_small.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<com.android.systemui.shared.clocks.AnimatableClockView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/animatable_clock_view"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="start"
+    android:gravity="start"
+    android:textSize="@dimen/small_clock_text_size"
+    android:fontFamily="@*android:string/config_clockFontFamily"
+    android:elegantTextHeight="false"
+    android:singleLine="true"
+    android:fontFeatureSettings="pnum"
+    chargeAnimationDelay="350"
+    dozeWeight="200"
+    lockScreenWeight="400" />
diff --git a/packages/SystemUI/shared/res/values/attrs.xml b/packages/SystemUI/shared/res/values/attrs.xml
new file mode 100644
index 0000000..f9d66ee
--- /dev/null
+++ b/packages/SystemUI/shared/res/values/attrs.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Formatting note: terminate all comments with a period, to avoid breaking
+     the documentation output. To suppress comment lines from the documentation
+     output, insert an eat-comment element after the comment lines.
+-->
+
+<resources>
+    <declare-styleable name="AnimatableClockView">
+        <attr name="dozeWeight" format="integer" />
+        <attr name="lockScreenWeight" format="integer" />
+        <attr name="chargeAnimationDelay" format="integer" />
+    </declare-styleable>
+</resources>
diff --git a/packages/SystemUI/shared/res/values/dimens.xml b/packages/SystemUI/shared/res/values/dimens.xml
new file mode 100644
index 0000000..8f90f0f
--- /dev/null
+++ b/packages/SystemUI/shared/res/values/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+*/
+-->
+<resources>
+    <!-- Clock maximum font size (dp is intentional, to prevent any further scaling) -->
+    <dimen name="large_clock_text_size">150sp</dimen>
+    <dimen name="small_clock_text_size">86sp</dimen>
+
+    <!-- Default line spacing multiplier between hours and minutes of the keyguard clock -->
+    <item name="keyguard_clock_line_spacing_scale" type="dimen" format="float">.7</item>
+    <!-- Burmese line spacing multiplier between hours and minutes of the keyguard clock -->
+    <item name="keyguard_clock_line_spacing_scale_burmese" type="dimen" format="float">1</item>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/shared/res/values/donottranslate.xml b/packages/SystemUI/shared/res/values/donottranslate.xml
new file mode 100644
index 0000000..383d5521
--- /dev/null
+++ b/packages/SystemUI/shared/res/values/donottranslate.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Skeleton string format for displaying the time in 12-hour format. -->
+    <string name="clock_12hr_format">hm</string>
+
+    <!-- Skeleton string format for displaying the time in 24-hour format. -->
+    <string name="clock_24hr_format">Hm</string>
+</resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
similarity index 90%
rename from packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt
rename to packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index b4955d2..2739d59 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.keyguard
+package com.android.systemui.shared.clocks
 
 import android.animation.TimeInterpolator
 import android.annotation.ColorInt
@@ -26,9 +26,10 @@
 import android.text.format.DateFormat
 import android.util.AttributeSet
 import android.widget.TextView
-import com.android.systemui.R
+import com.android.systemui.animation.GlyphCallback
 import com.android.systemui.animation.Interpolators
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import com.android.systemui.animation.TextAnimator
+import com.android.systemui.shared.R
 import java.io.PrintWriter
 import java.util.Calendar
 import java.util.Locale
@@ -38,13 +39,13 @@
  * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
  * The time's text color is a gradient that changes its colors based on its controller.
  */
+@SuppressLint("AppCompatCustomView")
 class AnimatableClockView @JvmOverloads constructor(
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-    private val tag = "AnimatableClockView"
 
     private var lastMeasureCall: CharSequence = ""
 
@@ -193,7 +194,7 @@
         )
     }
 
-    fun animateFoldAppear() {
+    fun animateFoldAppear(animate: Boolean = true) {
         if (textAnimator == null) {
             return
         }
@@ -210,22 +211,22 @@
             weight = dozingWeightInternal,
             textSize = -1f,
             color = dozingColor,
-            animate = true,
+            animate = animate,
             interpolator = Interpolators.EMPHASIZED_DECELERATE,
-            duration = StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
+            duration = ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
             delay = 0,
             onAnimationEnd = null
         )
     }
 
-    fun animateCharge(dozeStateGetter: DozeStateGetter) {
+    fun animateCharge(isDozing: () -> Boolean) {
         if (textAnimator == null || textAnimator!!.isRunning()) {
             // Skip charge animation if dozing animation is already playing.
             return
         }
         val startAnimPhase2 = Runnable {
             setTextStyle(
-                weight = if (dozeStateGetter.isDozing) dozingWeight else lockScreenWeight,
+                weight = if (isDozing()) dozingWeight else lockScreenWeight,
                 textSize = -1f,
                 color = null,
                 animate = true,
@@ -235,7 +236,7 @@
             )
         }
         setTextStyle(
-            weight = if (dozeStateGetter.isDozing) lockScreenWeight else dozingWeight,
+            weight = if (isDozing()) lockScreenWeight else dozingWeight,
             textSize = -1f,
             color = null,
             animate = true,
@@ -330,9 +331,9 @@
         )
     }
 
-    fun refreshFormat() {
+    fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
+    fun refreshFormat(use24HourFormat: Boolean) {
         Patterns.update(context)
-        val use24HourFormat = DateFormat.is24HourFormat(context)
 
         format = when {
             isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
@@ -385,14 +386,14 @@
         }
     }
 
-    interface DozeStateGetter {
-        val isDozing: Boolean
+    companion object {
+        private val TAG = AnimatableClockView::class.simpleName
+        const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
+        private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
+        private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
+        private const val DOZE_ANIM_DURATION: Long = 300
+        private const val APPEAR_ANIM_DURATION: Long = 350
+        private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
+        private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
     }
 }
-
-private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
-private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
-private const val DOZE_ANIM_DURATION: Long = 300
-private const val APPEAR_ANIM_DURATION: Long = 350
-private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
-private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockProviderPlugin.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockProviderPlugin.kt
deleted file mode 100644
index 916a557..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockProviderPlugin.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.shared.clocks
-
-import com.android.systemui.plugins.Plugin
-import com.android.systemui.plugins.annotations.ProvidesInterface
-import android.annotation.FloatRange
-import android.graphics.drawable.Drawable
-import android.view.View
-
-/** Identifies a clock design */
-typealias ClockId = String
-
-/** A Plugin which exposes the ClockProvider interface */
-@ProvidesInterface(action = ClockProviderPlugin.ACTION, version = ClockProviderPlugin.VERSION)
-interface ClockProviderPlugin : Plugin, ClockProvider {
-    companion object {
-        const val ACTION = "com.android.systemui.action.PLUGIN_CLOCK_PROVIDER"
-        const val VERSION = 1
-    }
-}
-
-/** Interface for building clocks and providing information about those clocks */
-interface ClockProvider {
-    /** Returns metadata for all clocks this provider knows about */
-    fun getClocks(): List<ClockMetadata>
-
-    /** Initializes and returns the target clock design */
-    fun createClock(id: ClockId): Clock
-
-    /** A static thumbnail for rendering in some examples */
-    fun getClockThumbnail(id: ClockId): Drawable?
-}
-
-/** Interface for controlling an active clock */
-interface Clock {
-    /** A small version of the clock, appropriate for smaller viewports */
-    val smallClock: View
-
-    /** A large version of the clock, appropriate when a bigger viewport is available */
-    val largeClock: View
-
-    /** Callback to update the clock view to the current time */
-    fun onTimeTick()
-
-    /** Sets the level of the AOD transition */
-    fun setAodFraction(@FloatRange(from = 0.0, to = 1.0) fraction: Float)
-}
-
-/** Some data about a clock design */
-data class ClockMetadata(
-    val clockId: ClockId,
-    val name: String
-)
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 3245966..a4c03b0 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -22,6 +22,11 @@
 import android.provider.Settings
 import android.util.Log
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.Clock
+import com.android.systemui.plugins.ClockId
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.plugins.ClockProvider
+import com.android.systemui.plugins.ClockProviderPlugin
 import com.android.systemui.plugins.PluginListener
 import com.android.systemui.shared.plugins.PluginManager
 import com.google.gson.Gson
@@ -29,16 +34,23 @@
 
 private val TAG = ClockRegistry::class.simpleName
 private val DEBUG = true
-const val DEFAULT_CLOCK_ID = "DEFAULT"
 
 typealias ClockChangeListener = () -> Unit
 
 /** ClockRegistry aggregates providers and plugins */
-open class ClockRegistry @Inject constructor(
+open class ClockRegistry(
     val context: Context,
     val pluginManager: PluginManager,
-    @Main val handler: Handler
+    val handler: Handler,
+    defaultClockProvider: ClockProvider
 ) {
+    @Inject constructor(
+        context: Context,
+        pluginManager: PluginManager,
+        @Main handler: Handler,
+        defaultClockProvider: DefaultClockProvider
+    ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { }
+
     private val gson = Gson()
     private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
@@ -48,60 +60,71 @@
     }
 
     private val pluginListener = object : PluginListener<ClockProviderPlugin> {
-        override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) {
-            val currentId = currentClockId
-            for (clock in plugin.getClocks()) {
-                val id = clock.clockId
-                val current = availableClocks[id]
-                if (current != null) {
-                    Log.e(TAG, "Clock Id conflict: $id is registered by both " +
-                            "${plugin::class.simpleName} and ${current.provider::class.simpleName}")
-                    return
-                }
+        override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
+            connectClocks(plugin)
 
-                availableClocks[id] = ClockInfo(clock, plugin)
-
-                if (currentId == id) {
-                    if (DEBUG) {
-                        Log.i(TAG, "Current clock ($currentId) was connected")
-                    }
-                    clockChangeListeners.forEach { it() }
-                }
-            }
-        }
-
-        override fun onPluginDisconnected(plugin: ClockProviderPlugin) {
-            val currentId = currentClockId
-            for (clock in plugin.getClocks()) {
-                availableClocks.remove(clock.clockId)
-
-                if (currentId == clock.clockId) {
-                    Log.w(TAG, "Current clock ($currentId) was disconnected")
-                    clockChangeListeners.forEach { it() }
-                }
-            }
-        }
+        override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
+            disconnectClocks(plugin)
     }
 
     open var currentClockId: ClockId
         get() {
-            val json = Settings.Secure.getString(context.contentResolver,
-                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE)
+            val json = Settings.Secure.getString(
+                context.contentResolver,
+                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
+            )
             return gson.fromJson(json, ClockSetting::class.java).clockId
         }
         set(value) {
             val json = gson.toJson(ClockSetting(value, System.currentTimeMillis()))
-            Settings.Secure.putString(context.contentResolver,
-                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json)
+            Settings.Secure.putString(
+                context.contentResolver,
+                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
+            )
         }
 
     init {
+        connectClocks(defaultClockProvider)
         pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java)
         context.contentResolver.registerContentObserver(
             Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
             false,
             settingObserver,
-            UserHandle.USER_ALL)
+            UserHandle.USER_ALL
+        )
+    }
+
+    private fun connectClocks(provider: ClockProvider) {
+        val currentId = currentClockId
+        for (clock in provider.getClocks()) {
+            val id = clock.clockId
+            val current = availableClocks[id]
+            if (current != null) {
+                Log.e(TAG, "Clock Id conflict: $id is registered by both " +
+                    "${provider::class.simpleName} and ${current.provider::class.simpleName}")
+                return
+            }
+
+            availableClocks[id] = ClockInfo(clock, provider)
+            if (currentId == id) {
+                if (DEBUG) {
+                    Log.i(TAG, "Current clock ($currentId) was connected")
+                }
+                clockChangeListeners.forEach { it() }
+            }
+        }
+    }
+
+    private fun disconnectClocks(provider: ClockProvider) {
+        val currentId = currentClockId
+        for (clock in provider.getClocks()) {
+            availableClocks.remove(clock.clockId)
+
+            if (currentId == clock.clockId) {
+                Log.w(TAG, "Current clock ($currentId) was disconnected")
+                clockChangeListeners.forEach { it() }
+            }
+        }
     }
 
     fun getClocks(): List<ClockMetadata> = availableClocks.map { (_, clock) -> clock.metadata }
@@ -143,4 +166,4 @@
         val clockId: ClockId,
         val _applied_timestamp: Long
     )
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
new file mode 100644
index 0000000..5d8da59
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.shared.clocks
+
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.icu.text.NumberFormat
+import android.util.TypedValue
+import android.view.LayoutInflater
+import com.android.internal.colorextraction.ColorExtractor
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.Clock
+import com.android.systemui.plugins.ClockAnimation
+import com.android.systemui.plugins.ClockEvents
+import com.android.systemui.plugins.ClockId
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.plugins.ClockProvider
+import com.android.systemui.shared.R
+import java.io.PrintWriter
+import java.util.Locale
+import java.util.TimeZone
+import javax.inject.Inject
+
+private val TAG = DefaultClockProvider::class.simpleName
+const val DEFAULT_CLOCK_NAME = "Default Clock"
+const val DEFAULT_CLOCK_ID = "DEFAULT"
+
+/** Provides the default system clock */
+class DefaultClockProvider @Inject constructor(
+    val layoutInflater: LayoutInflater,
+    @Main val resources: Resources
+) : ClockProvider {
+    override fun getClocks(): List<ClockMetadata> =
+        listOf(ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME))
+
+    override fun createClock(id: ClockId): Clock {
+        if (id != DEFAULT_CLOCK_ID) {
+            throw IllegalArgumentException("$id is unsupported by $TAG")
+        }
+        return DefaultClock(layoutInflater, resources)
+    }
+
+    override fun getClockThumbnail(id: ClockId): Drawable? {
+        if (id != DEFAULT_CLOCK_ID) {
+            throw IllegalArgumentException("$id is unsupported by $TAG")
+        }
+
+        // TODO: Update placeholder to actual resource
+        return resources.getDrawable(R.drawable.clock_default_thumbnail, null)
+    }
+}
+
+/**
+ * Controls the default clock visuals.
+ *
+ * This serves as an adapter between the clock interface and the
+ * AnimatableClockView used by the existing lockscreen clock.
+ */
+class DefaultClock(
+    private val layoutInflater: LayoutInflater,
+    private val resources: Resources
+) : Clock {
+    override val smallClock =
+        layoutInflater.inflate(R.layout.clock_default_small, null) as AnimatableClockView
+    override val largeClock =
+        layoutInflater.inflate(R.layout.clock_default_large, null) as AnimatableClockView
+    private val clocks = listOf(smallClock, largeClock)
+
+    private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my"))
+    private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong())
+    private val burmeseLineSpacing =
+        resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese)
+    private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale)
+
+    override val events = object : ClockEvents {
+        override fun onTimeTick() = clocks.forEach { it.refreshTime() }
+
+        override fun onTimeFormatChanged(is24Hr: Boolean) =
+            clocks.forEach { it.refreshFormat(is24Hr) }
+
+        override fun onTimeZoneChanged(timeZone: TimeZone) =
+            clocks.forEach { it.onTimeZoneChanged(timeZone) }
+
+        override fun onFontSettingChanged() {
+            smallClock.setTextSize(
+                TypedValue.COMPLEX_UNIT_PX,
+                resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
+            )
+            largeClock.setTextSize(
+                TypedValue.COMPLEX_UNIT_PX,
+                resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
+            )
+        }
+
+        override fun onColorPaletteChanged(palette: ColorExtractor.GradientColors) =
+            clocks.forEach { it.setColors(DOZE_COLOR, palette.mainColor) }
+
+        override fun onLocaleChanged(locale: Locale) {
+            val nf = NumberFormat.getInstance(locale)
+            if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
+                clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) }
+            } else {
+                clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) }
+            }
+
+            clocks.forEach { it.refreshFormat() }
+        }
+    }
+
+    override val animation = object : ClockAnimation {
+        override fun initialize(dozeFraction: Float, foldFraction: Float) {
+            dozeState = AnimationState(dozeFraction)
+            foldState = AnimationState(foldFraction)
+
+            if (foldState.isActive) {
+                clocks.forEach { it.animateFoldAppear(false) }
+            } else {
+                clocks.forEach { it.animateDoze(dozeState.isActive, false) }
+            }
+        }
+
+        override fun enter() {
+            if (dozeState.isActive) {
+                clocks.forEach { it.animateAppearOnLockscreen() }
+            }
+        }
+
+        override fun charge() = clocks.forEach { it.animateCharge { dozeState.isActive } }
+
+        private var foldState = AnimationState(0f)
+        override fun fold(fraction: Float) {
+            val (hasChanged, hasJumped) = foldState.update(fraction)
+            if (hasChanged) {
+                clocks.forEach { it.animateFoldAppear(!hasJumped) }
+            }
+        }
+
+        private var dozeState = AnimationState(0f)
+        override fun doze(fraction: Float) {
+            val (hasChanged, hasJumped) = dozeState.update(fraction)
+            if (hasChanged) {
+                clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) }
+            }
+        }
+    }
+
+    private class AnimationState(
+        var fraction: Float
+    ) {
+        var isActive: Boolean = fraction < 0.5f
+        fun update(newFraction: Float): Pair<Boolean, Boolean> {
+            val wasActive = isActive
+            val hasJumped = (fraction == 0f && newFraction == 1f) ||
+                (fraction == 1f && newFraction == 0f)
+            isActive = newFraction > fraction
+            fraction = newFraction
+            return Pair(wasActive != isActive, hasJumped)
+        }
+    }
+
+    init {
+        events.onLocaleChanged(Locale.getDefault())
+    }
+
+    override fun dump(pw: PrintWriter) = clocks.forEach { it.dump(pw) }
+
+    companion object {
+        private const val DOZE_COLOR = Color.WHITE
+        private const val FORMAT_NUMBER = 1234567890
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index e3f5687..4222744 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shared.recents.model;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
@@ -245,12 +246,16 @@
      */
     public static Task from(TaskKey taskKey, TaskInfo taskInfo, boolean isLocked) {
         ActivityManager.TaskDescription td = taskInfo.taskDescription;
+        // Also consider undefined activity type to include tasks in overview right after rebooting
+        // the device.
+        final boolean isDockable = taskInfo.supportsMultiWindow
+                && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode())
+                && (taskInfo.getActivityType() == ACTIVITY_TYPE_UNDEFINED
+                || ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()));
         return new Task(taskKey,
                 td != null ? td.getPrimaryColor() : 0,
-                td != null ? td.getBackgroundColor() : 0, taskInfo.supportsMultiWindow
-                && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
-                && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode()),
-                isLocked, td, taskInfo.topActivity);
+                td != null ? td.getBackgroundColor() : 0, isDockable , isLocked, td,
+                taskInfo.topActivity);
     }
 
     public Task(TaskKey key) {
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
index 487e1a4..c69ff7e 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
@@ -32,6 +32,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.ViewController;
 
@@ -134,6 +135,21 @@
                 reset();
             }
         }
+
+        @Override
+        public void onTimeFormatChanged(String timeFormat) {
+            mView.refreshFormat();
+        }
+
+        @Override
+        public void onTimeZoneChanged(TimeZone timeZone) {
+            mView.onTimeZoneChanged(timeZone);
+        }
+
+        @Override
+        public void onUserSwitchComplete(int userId) {
+            mView.refreshFormat();
+        }
     };
 
     @Override
@@ -186,7 +202,7 @@
     /** Animate the clock appearance when a foldable device goes from fully-open/half-open state to
      * fully folded state and it goes to sleep (always on display screen) */
     public void animateFoldAppear() {
-        mView.animateFoldAppear();
+        mView.animateFoldAppear(true);
     }
 
     /**
@@ -197,20 +213,6 @@
     }
 
     /**
-     * Updates the timezone for the view.
-     */
-    public void onTimeZoneChanged(TimeZone timeZone) {
-        mView.onTimeZoneChanged(timeZone);
-    }
-
-    /**
-     * Trigger a time format update
-     */
-    public void refreshFormat() {
-        mView.refreshFormat();
-    }
-
-    /**
      * Return locallly stored dozing state.
      */
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 40edfe5..206b8be 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -22,6 +22,7 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.plugins.ClockPlugin;
+import com.android.systemui.shared.clocks.AnimatableClockView;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 5c9dd5e..6c32a49 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -398,17 +398,6 @@
 
     void updateTimeZone(TimeZone timeZone) {
         mView.onTimeZoneChanged(timeZone);
-        if (mClockViewController != null) {
-            mClockViewController.onTimeZoneChanged(timeZone);
-            mLargeClockViewController.onTimeZoneChanged(timeZone);
-        }
-    }
-
-    void refreshFormat() {
-        if (mClockViewController != null) {
-            mClockViewController.refreshFormat();
-            mLargeClockViewController.refreshFormat();
-        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 8921780..014d082 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -240,11 +240,6 @@
         }
 
         @Override
-        public void onTimeFormatChanged(String timeFormat) {
-            mKeyguardClockSwitchController.refreshFormat();
-        }
-
-        @Override
         public void onTimeZoneChanged(TimeZone timeZone) {
             mKeyguardClockSwitchController.updateTimeZone(timeZone);
         }
@@ -256,11 +251,6 @@
                 refreshTime();
             }
         }
-
-        @Override
-        public void onUserSwitchComplete(int userId) {
-            mKeyguardClockSwitchController.refreshFormat();
-        }
     };
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index c191757..4ae2cad 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -805,8 +805,8 @@
     private Runnable mRetryFingerprintAuthentication = new Runnable() {
         @Override
         public void run() {
-            Log.w(TAG, "Retrying fingerprint after HW unavailable, attempt " +
-                    mHardwareFingerprintUnavailableRetryCount);
+            Log.w(TAG,
+                    "Retrying fingerprint attempt: " + mHardwareFingerprintUnavailableRetryCount);
             if (mFpm.isHardwareDetected()) {
                 updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
             } else if (mHardwareFingerprintUnavailableRetryCount < HAL_ERROR_RETRY_MAX) {
@@ -833,7 +833,9 @@
             setFingerprintRunningState(BIOMETRIC_STATE_STOPPED);
         }
 
-        if (msgId == FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE) {
+        if (msgId == FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE
+                || msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED) {
+            Log.d(TAG, "Fingerprint retrying auth due to(" + msgId + ") -> " + errString);
             mHandler.postDelayed(mRetryFingerprintAuthentication, HAL_ERROR_RETRY_TIMEOUT);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index f99293a..edcaf49 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -842,7 +842,8 @@
                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                         | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                         | WindowManager.LayoutParams.FLAG_SLIPPERY
-                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                 PixelFormat.TRANSLUCENT);
         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
                 | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index aa67ecd..e16ac08 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -169,6 +169,7 @@
     private Animator mExitAnimator;
     private Animator mEnterAnimator;
     private final int mOrientation;
+    private boolean mKeyboardVisible;
 
 
     public ClipboardOverlayController(Context context,
@@ -261,8 +262,22 @@
         attachWindow();
         withWindowAttached(() -> {
             mWindow.setContentView(mView);
-            updateInsets(mWindowManager.getCurrentWindowMetrics().getWindowInsets());
-            mView.requestLayout();
+            WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+            mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
+            updateInsets(insets);
+            mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(
+                    new ViewTreeObserver.OnGlobalLayoutListener() {
+                        @Override
+                        public void onGlobalLayout() {
+                            WindowInsets insets =
+                                    mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+                            boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
+                            if (keyboardVisible != mKeyboardVisible) {
+                                mKeyboardVisible = keyboardVisible;
+                                updateInsets(insets);
+                            }
+                        }
+                    });
             mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
                     new ViewRootImpl.ActivityConfigCallback() {
                         @Override
@@ -384,8 +399,6 @@
             mRemoteCopyChip.setVisibility(View.GONE);
         }
         withWindowAttached(() -> {
-            updateInsets(
-                    mWindowManager.getCurrentWindowMetrics().getWindowInsets());
             if (mEnterAnimator == null || !mEnterAnimator.isRunning()) {
                 mView.post(this::animateIn);
             }
@@ -509,7 +522,7 @@
     private void shareContent(ClipData clip) {
         mUiEventLogger.log(CLIPBOARD_OVERLAY_SHARE_TAPPED);
         Intent shareIntent = new Intent(Intent.ACTION_SEND);
-        shareIntent.putExtra(Intent.EXTRA_TEXT, clip.getItemAt(0).getText());
+        shareIntent.putExtra(Intent.EXTRA_TEXT, clip.getItemAt(0).getText().toString());
         shareIntent.setDataAndType(
                 clip.getItemAt(0).getUri(), clip.getDescription().getMimeType(0));
         shareIntent.putExtra(Intent.EXTRA_STREAM, clip.getItemAt(0).getUri());
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
index a174ed0..2389ad1 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
@@ -62,7 +62,7 @@
             backgroundExecutor.execute {
                 val collator = Collator.getInstance(resources.configuration.locales[0])
                 val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) {
-                    it.loadLabel()
+                    it.loadLabel() ?: ""
                 }
                 listOfServices = serviceInfos.sortedWith(localeComparator)
                 uiExecutor.execute(::notifyDataSetChanged)
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 029cabb..718befa 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -31,6 +31,7 @@
 import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver;
 import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender;
 import com.android.systemui.people.PeopleProvider;
+import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.unfold.FoldStateLogger;
 import com.android.systemui.unfold.FoldStateLoggingProvider;
@@ -130,6 +131,7 @@
         getMediaTttCommandLineHelper();
         getMediaMuteAwaitConnectionCli();
         getNearbyMediaDevicesManager();
+        getConnectivityInfoProcessor();
         getUnfoldLatencyTracker().init();
         getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init);
         getFoldStateLogger().ifPresent(FoldStateLogger::init);
@@ -212,6 +214,9 @@
     /** */
     Optional<NearbyMediaDevicesManager> getNearbyMediaDevicesManager();
 
+    /** */
+    Optional<ConnectivityInfoProcessor> getConnectivityInfoProcessor();
+
     /**
      * Returns {@link CoreStartable}s that should be started with the application.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index fe96222..ceb702e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -70,7 +70,7 @@
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
-import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -91,7 +91,6 @@
 import com.android.systemui.wmshell.BubblesManager;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.dagger.DynamicOverride;
-import com.android.wm.shell.sysui.ShellController;
 
 import java.util.Optional;
 import java.util.concurrent.Executor;
@@ -135,6 +134,7 @@
             SettingsUtilModule.class,
             SmartRepliesInflationModule.class,
             SmartspaceModule.class,
+            StatusBarPipelineModule.class,
             StatusBarPolicyModule.class,
             StatusBarWindowModule.class,
             SysUIConcurrencyModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 65711ed..958a219 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -61,7 +61,7 @@
             new ResourceBooleanFlag(108, R.bool.config_notificationToContents);
 
     public static final BooleanFlag REMOVE_UNRANKED_NOTIFICATIONS =
-            new BooleanFlag(109, false);
+            new BooleanFlag(109, false, true);
 
     public static final BooleanFlag FSI_REQUIRES_KEYGUARD =
             new BooleanFlag(110, false, true);
@@ -148,6 +148,8 @@
     public static final BooleanFlag STATUS_BAR_LETTERBOX_APPEARANCE =
             new BooleanFlag(603, false);
 
+    public static final BooleanFlag NEW_STATUS_BAR_PIPELINE = new BooleanFlag(604, false);
+
     /***************************************/
     // 700 - dialer/calls
     public static final BooleanFlag ONGOING_CALL_STATUS_BAR_CHIP =
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 2006d53..b05e75e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -601,7 +601,7 @@
                             if (currentState == GestureState.INACTIVE ||
                                 currentState == GestureState.CANCELLED
                             )
-                                params.entryIndicator.backgroundDimens.edgeCornerRadius
+                                params.cancelledEdgeCornerRadius
                             else
                                 params.activeIndicator.backgroundDimens.edgeCornerRadius
                         )
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
index a3fb58d..d56537b 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
@@ -33,6 +33,8 @@
         private set
     var fullyStretchedIndicator = BackIndicatorDimens()
         private set
+    var cancelledEdgeCornerRadius: Float = 0f
+        private set
     var cancelledArrowDimens = ArrowDimens()
 
     // navigation bar edge constants
@@ -132,6 +134,8 @@
             )
         )
 
+        cancelledEdgeCornerRadius = getDimen(R.dimen.navigation_edge_cancelled_edge_corners)
+
         cancelledArrowDimens = ArrowDimens(
             length = getDimen(R.dimen.navigation_edge_cancelled_arrow_length),
             height = getDimen(R.dimen.navigation_edge_cancelled_arrow_height)
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index e845aa8..7cc95a1 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -22,7 +22,6 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.util.Log;
-import android.view.View;
 import android.view.ViewGroup;
 
 import androidx.activity.ComponentActivity;
@@ -40,7 +39,6 @@
     private static final boolean DEBUG = PeopleSpaceUtils.DEBUG;
 
     private final PeopleViewModel.Factory mViewModelFactory;
-    private PeopleViewModel mViewModel;
 
     @Inject
     public PeopleSpaceActivity(PeopleViewModel.Factory viewModelFactory) {
@@ -52,38 +50,32 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setResult(RESULT_CANCELED);
-        mViewModel = new ViewModelProvider(this, mViewModelFactory).get(PeopleViewModel.class);
+
+        PeopleViewModel viewModel = new ViewModelProvider(this, mViewModelFactory).get(
+                PeopleViewModel.class);
 
         // Update the widget ID coming from the intent.
         int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
-        mViewModel.onWidgetIdChanged(widgetId);
+        viewModel.onWidgetIdChanged(widgetId);
 
         ViewGroup view = PeopleViewBinder.create(this);
-        PeopleViewBinder.bind(view, mViewModel, /* lifecycleOwner= */ this,
-                () -> {
-                    finishActivity();
+        PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this,
+                (result) -> {
+                    finishActivity(result);
                     return null;
                 });
         setContentView(view);
     }
 
-    /** Finish activity with a successful widget configuration result. */
-    private void finishActivity() {
-        if (DEBUG) Log.d(TAG, "Widget added!");
-        setActivityResult(RESULT_OK);
+    private void finishActivity(PeopleViewModel.Result result) {
+        if (result instanceof PeopleViewModel.Result.Success) {
+            if (DEBUG) Log.d(TAG, "Widget added!");
+            Intent data = ((PeopleViewModel.Result.Success) result).getData();
+            setResult(RESULT_OK, data);
+        } else {
+            if (DEBUG) Log.d(TAG, "Activity dismissed with no widgets added!");
+            setResult(RESULT_CANCELED);
+        }
         finish();
     }
-
-    /** Finish activity without choosing a widget. */
-    public void dismissActivity(View v) {
-        if (DEBUG) Log.d(TAG, "Activity dismissed with no widgets added!");
-        setResult(RESULT_CANCELED);
-        finish();
-    }
-
-    private void setActivityResult(int result) {
-        Intent resultValue = new Intent();
-        resultValue.putExtra(EXTRA_APPWIDGET_ID, mViewModel.getAppWidgetId().getValue());
-        setResult(result, resultValue);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
index bc982cc..d8a429e 100644
--- a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
@@ -41,7 +41,7 @@
 
 /** A ViewBinder for [PeopleViewModel]. */
 object PeopleViewBinder {
-    private const val TAG = "PeopleSpaceViewBinder"
+    private const val TAG = "PeopleViewBinder"
 
     /**
      * The [ViewOutlineProvider] used to clip the corner radius of the recent and priority lists.
@@ -72,15 +72,15 @@
         view: ViewGroup,
         viewModel: PeopleViewModel,
         lifecycleOwner: LifecycleOwner,
-        onFinish: () -> Unit,
+        onResult: (PeopleViewModel.Result) -> Unit,
     ) {
-        // Call [onFinish] this activity when the ViewModel tells us so.
+        // Call [onResult] as soon as a result is available.
         lifecycleOwner.lifecycleScope.launch {
             lifecycleOwner.repeatOnLifecycle(CREATED) {
-                viewModel.isFinished.collect { isFinished ->
-                    if (isFinished) {
-                        viewModel.clearIsFinished()
-                        onFinish()
+                viewModel.result.collect { result ->
+                    if (result != null) {
+                        viewModel.clearResult()
+                        onResult(result)
                     }
                 }
             }
@@ -104,7 +104,7 @@
                                 viewModel::onTileClicked,
                             )
                         } else {
-                            setNoConversationsContent(view)
+                            setNoConversationsContent(view, viewModel::onUserJourneyCancelled)
                         }
                     }
             }
@@ -119,7 +119,7 @@
         }
     }
 
-    private fun setNoConversationsContent(view: ViewGroup) {
+    private fun setNoConversationsContent(view: ViewGroup, onGotItClicked: () -> Unit) {
         // This should never happen.
         if (view.childCount > 1) {
             error("view has ${view.childCount} children, it should have maximum 1")
@@ -140,6 +140,10 @@
             LayoutInflater.from(context)
                 .inflate(R.layout.people_space_activity_no_conversations, /* root= */ view)
 
+        noConversationsView.findViewById<View>(R.id.got_it_button).setOnClickListener {
+            onGotItClicked()
+        }
+
         // The Tile preview has colorBackground as its background. Change it so it's different than
         // the activity's background.
         val item = noConversationsView.findViewById<LinearLayout>(android.R.id.background)
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
index 17de991..0834a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.people.ui.viewmodel
 
+import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
 import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
 import android.content.Context
+import android.content.Intent
 import android.util.Log
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
@@ -32,6 +34,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 
 /**
  * Models UI state for the people space, allowing the user to select which conversation should be
@@ -49,7 +52,7 @@
      * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
      */
     private val _priorityTiles = MutableStateFlow(priorityTiles())
-    val priorityTiles: Flow<List<PeopleTileViewModel>> = _priorityTiles
+    val priorityTiles: Flow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
 
     /**
      * The list of the priority tiles/conversations.
@@ -58,15 +61,15 @@
      * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
      */
     private val _recentTiles = MutableStateFlow(recentTiles())
-    val recentTiles: Flow<List<PeopleTileViewModel>> = _recentTiles
+    val recentTiles: Flow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
 
     /** The ID of the widget currently being edited/added. */
     private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
-    val appWidgetId: StateFlow<Int> = _appWidgetId
+    val appWidgetId: StateFlow<Int> = _appWidgetId.asStateFlow()
 
-    /** Whether the user journey is complete. */
-    private val _isFinished = MutableStateFlow(false)
-    val isFinished: StateFlow<Boolean> = _isFinished
+    /** The result of this user journey. */
+    private val _result = MutableStateFlow<Result?>(null)
+    val result: StateFlow<Result?> = _result.asStateFlow()
 
     /** Refresh the [priorityTiles] and [recentTiles]. */
     fun onTileRefreshRequested() {
@@ -79,22 +82,28 @@
         _appWidgetId.value = widgetId
     }
 
-    /** Clear [isFinished], setting it to false. */
-    fun clearIsFinished() {
-        _isFinished.value = false
+    /** Clear [result], setting it to null. */
+    fun clearResult() {
+        _result.value = null
     }
 
     /** Called when a tile is clicked. */
     fun onTileClicked(tile: PeopleTileViewModel) {
+        val widgetId = _appWidgetId.value
         if (PeopleSpaceUtils.DEBUG) {
             Log.d(
                 TAG,
-                "Put ${tile.username}'s shortcut ID: ${tile.key.shortcutId} for widget ID: " +
-                    _appWidgetId.value
+                "Put ${tile.username}'s shortcut ID: ${tile.key.shortcutId} for widget ID $widgetId"
             )
         }
-        widgetRepository.setWidgetTile(_appWidgetId.value, tile.key)
-        _isFinished.value = true
+        widgetRepository.setWidgetTile(widgetId, tile.key)
+        _result.value =
+            Result.Success(Intent().apply { putExtra(EXTRA_APPWIDGET_ID, appWidgetId.value) })
+    }
+
+    /** Called when this user journey is cancelled. */
+    fun onUserJourneyCancelled() {
+        _result.value = Result.Cancelled
     }
 
     private fun priorityTiles(): List<PeopleTileViewModel> {
@@ -143,7 +152,12 @@
         }
     }
 
+    sealed class Result {
+        class Success(val data: Intent) : Result()
+        object Cancelled : Result()
+    }
+
     companion object {
-        private const val TAG = "PeopleSpaceViewModel"
+        private const val TAG = "PeopleViewModel"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index a1c66b3..4552abd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -19,8 +19,6 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings.Secure;
@@ -28,6 +26,7 @@
 import android.util.ArraySet;
 import android.util.Log;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.InstanceId;
@@ -35,9 +34,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.PluginListener;
@@ -68,12 +65,20 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.Executor;
 import java.util.function.Predicate;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-/** Platform implementation of the quick settings tile host **/
+/** Platform implementation of the quick settings tile host
+ *
+ * This class keeps track of the set of current tiles and is the in memory source of truth
+ * (ground truth is kept in {@link Secure#QS_TILES}). When the ground truth changes,
+ * {@link #onTuningChanged} will be called and the tiles will be re-created as needed.
+ *
+ * This class also provides the interface for adding/removing/changing tiles.
+ */
 @SysUISingleton
 public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable {
     private static final String TAG = "QSTileHost";
@@ -89,11 +94,11 @@
     private final TunerService mTunerService;
     private final PluginManager mPluginManager;
     private final DumpManager mDumpManager;
-    private final BroadcastDispatcher mBroadcastDispatcher;
     private final QSLogger mQSLogger;
     private final UiEventLogger mUiEventLogger;
     private final InstanceIdSequence mInstanceIdSequence;
     private final CustomTileStatePersister mCustomTileStatePersister;
+    private final Executor mMainExecutor;
 
     private final List<Callback> mCallbacks = new ArrayList<>();
     @Nullable
@@ -113,13 +118,11 @@
     public QSTileHost(Context context,
             StatusBarIconController iconController,
             QSFactory defaultFactory,
-            @Main Handler mainHandler,
-            @Background Looper bgLooper,
+            @Main Executor mainExecutor,
             PluginManager pluginManager,
             TunerService tunerService,
             Provider<AutoTileManager> autoTiles,
             DumpManager dumpManager,
-            BroadcastDispatcher broadcastDispatcher,
             Optional<CentralSurfaces> centralSurfacesOptional,
             QSLogger qsLogger,
             UiEventLogger uiEventLogger,
@@ -137,7 +140,7 @@
         mDumpManager = dumpManager;
         mQSLogger = qsLogger;
         mUiEventLogger = uiEventLogger;
-        mBroadcastDispatcher = broadcastDispatcher;
+        mMainExecutor = mainExecutor;
         mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this);
         mTileLifeCycleManagerFactory = tileLifecycleManagerFactory;
 
@@ -151,7 +154,7 @@
         mSecureSettings = secureSettings;
         mCustomTileStatePersister = customTileStatePersister;
 
-        mainHandler.post(() -> {
+        mainExecutor.execute(() -> {
             // This is technically a hack to avoid circular dependency of
             // QSTileHost -> XXXTile -> QSTileHost. Posting ensures creation
             // finishes before creating any tiles.
@@ -258,6 +261,33 @@
         return mTileSpecs.indexOf(spec);
     }
 
+    /**
+     * Whenever the Secure Setting keeping track of the current tiles changes (or upon start) this
+     * will be called with the new value of the setting.
+     *
+     * This method will do the following:
+     * <ol>
+     *     <li>Destroy any existing tile that's not one of the current tiles (in the setting)</li>
+     *     <li>Create new tiles for those that don't already exist. If this tiles end up being
+     *         not available, they'll also be destroyed.</li>
+     *     <li>Save the resolved list of tiles (current tiles that are available) into the setting.
+     *         This means that after this call ends, the tiles in the Setting, {@link #mTileSpecs},
+     *         and visible tiles ({@link #mTiles}) must match.
+     *         </li>
+     * </ol>
+     *
+     * Additionally, if the user has changed, it'll do the following:
+     * <ul>
+     *     <li>Change the user for SystemUI tiles: {@link QSTile#userSwitch}.</li>
+     *     <li>Destroy any {@link CustomTile} and recreate it for the new user.</li>
+     * </ul>
+     *
+     * This happens in main thread as {@link com.android.systemui.tuner.TunerServiceImpl} dispatches
+     * in main thread.
+     *
+     * @see QSTile#isAvailable
+     */
+    @MainThread
     @Override
     public void onTuningChanged(String key, String newValue) {
         if (!TILES_SETTING.equals(key)) {
@@ -330,34 +360,44 @@
         mCurrentUser = currentUser;
         List<String> currentSpecs = new ArrayList<>(mTileSpecs);
         mTileSpecs.clear();
-        mTileSpecs.addAll(tileSpecs);
+        mTileSpecs.addAll(newTiles.keySet()); // Only add the valid (available) tiles.
         mTiles.clear();
         mTiles.putAll(newTiles);
         if (newTiles.isEmpty() && !tileSpecs.isEmpty()) {
             // If we didn't manage to create any tiles, set it to empty (default)
             Log.d(TAG, "No valid tiles on tuning changed. Setting to default.");
-            changeTiles(currentSpecs, loadTileSpecs(mContext, ""));
+            changeTilesByUser(currentSpecs, loadTileSpecs(mContext, ""));
         } else {
+            String resolvedTiles = TextUtils.join(",", mTileSpecs);
+            if (!resolvedTiles.equals(newValue)) {
+                // If the resolved tiles (those we actually ended up with) are different than
+                // the ones that are in the setting, update the Setting.
+                saveTilesToSettings(mTileSpecs);
+            }
             for (int i = 0; i < mCallbacks.size(); i++) {
                 mCallbacks.get(i).onTilesChanged();
             }
         }
     }
 
+    /**
+     * Only use with [CustomTile] if the tile doesn't exist anymore (and therefore doesn't need
+     * its lifecycle terminated).
+     */
     @Override
     public void removeTile(String spec) {
-        changeTileSpecs(tileSpecs-> tileSpecs.remove(spec));
+        mMainExecutor.execute(() -> changeTileSpecs(tileSpecs-> tileSpecs.remove(spec)));
     }
 
     /**
      * Remove many tiles at once.
      *
-     * It will only save to settings once (as opposed to {@link QSTileHost#removeTile} called
+     * It will only save to settings once (as opposed to {@link QSTileHost#removeTileByUser} called
      * multiple times).
      */
     @Override
     public void removeTiles(Collection<String> specs) {
-        changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs));
+        mMainExecutor.execute(() -> changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs)));
     }
 
     @Override
@@ -381,26 +421,29 @@
      * @param requestPosition -1 for end, 0 for beginning, or X for insertion at position X
      */
     public void addTile(String spec, int requestPosition) {
-        if (spec.equals("work")) Log.wtfStack(TAG, "Adding work tile");
-        changeTileSpecs(tileSpecs -> {
-            if (tileSpecs.contains(spec)) return false;
+        mMainExecutor.execute(() ->
+                changeTileSpecs(tileSpecs -> {
+                    if (tileSpecs.contains(spec)) return false;
 
-            int size = tileSpecs.size();
-            if (requestPosition == POSITION_AT_END || requestPosition >= size) {
-                tileSpecs.add(spec);
-            } else {
-                tileSpecs.add(requestPosition, spec);
-            }
-            return true;
-        });
+                    int size = tileSpecs.size();
+                    if (requestPosition == POSITION_AT_END || requestPosition >= size) {
+                        tileSpecs.add(spec);
+                    } else {
+                        tileSpecs.add(requestPosition, spec);
+                    }
+                    return true;
+                })
+        );
     }
 
-    void saveTilesToSettings(List<String> tileSpecs) {
+    @MainThread
+    private void saveTilesToSettings(List<String> tileSpecs) {
         mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs),
                 null /* tag */, false /* default */, mCurrentUser,
                 true /* overrideable by restore */);
     }
 
+    @MainThread
     private void changeTileSpecs(Predicate<List<String>> changeFunction) {
         final String setting = mSecureSettings.getStringForUser(TILES_SETTING, mCurrentUser);
         final List<String> tileSpecs = loadTileSpecs(mContext, setting);
@@ -420,29 +463,32 @@
      */
     public void addTile(ComponentName tile, boolean end) {
         String spec = CustomTile.toSpec(tile);
-        if (!mTileSpecs.contains(spec)) {
-            List<String> newSpecs = new ArrayList<>(mTileSpecs);
-            if (end) {
-                newSpecs.add(spec);
-            } else {
-                newSpecs.add(0, spec);
-            }
-            changeTiles(mTileSpecs, newSpecs);
-        }
+        addTile(spec, end ? POSITION_AT_END : 0);
     }
 
-    public void removeTile(ComponentName tile) {
-        List<String> newSpecs = new ArrayList<>(mTileSpecs);
-        newSpecs.remove(CustomTile.toSpec(tile));
-        changeTiles(mTileSpecs, newSpecs);
+    /**
+     * This will call through {@link #changeTilesByUser}. It should only be used when a tile is
+     * removed by a <b>user action</b> like {@code adb}.
+     */
+    public void removeTileByUser(ComponentName tile) {
+        mMainExecutor.execute(() -> {
+            List<String> newSpecs = new ArrayList<>(mTileSpecs);
+            if (newSpecs.remove(CustomTile.toSpec(tile))) {
+                changeTilesByUser(mTileSpecs, newSpecs);
+            }
+        });
     }
 
     /**
      * Change the tiles triggered by the user editing.
      * <p>
      * This is not called on device start, or on user change.
+     *
+     * {@link android.service.quicksettings.TileService#onTileRemoved} will be called for tiles
+     * that are removed.
      */
-    public void changeTiles(List<String> previousTiles, List<String> newTiles) {
+    @MainThread
+    public void changeTilesByUser(List<String> previousTiles, List<String> newTiles) {
         final List<String> copy = new ArrayList<>(previousTiles);
         final int NP = copy.size();
         for (int i = 0; i < NP; i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index e52bfbd..d84b12c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -182,7 +182,7 @@
         for (int i = 1; i < mTiles.size() && mTiles.get(i) != null; i++) {
             newSpecs.add(mTiles.get(i).spec);
         }
-        host.changeTiles(mCurrentSpecs, newSpecs);
+        host.changeTilesByUser(mCurrentSpecs, newSpecs);
         mCurrentSpecs = newSpecs;
     }
 
@@ -200,7 +200,7 @@
     /** */
     public void resetTileSpecs(List<String> specs) {
         // Notify the host so the tiles get removed callbacks.
-        mHost.changeTiles(mCurrentSpecs, specs);
+        mHost.changeTilesByUser(mCurrentSpecs, specs);
         setTileSpecs(specs);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index bf565a8..cfc57db 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -289,7 +289,7 @@
                 }
             }
 
-            mServices.getHost().removeTile(component);
+            mServices.getHost().removeTile(CustomTile.toSpec(component));
         }
     };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 89a15f6..82de389 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -843,14 +843,20 @@
         // The media player creation is slow and needs on the background thread.
         return CallbackToFutureAdapter.getFuture((completer) -> {
             mBgExecutor.execute(() -> {
-                MediaPlayer player = MediaPlayer.create(mContext,
-                        Uri.fromFile(new File(mContext.getResources().getString(
-                                com.android.internal.R.string.config_cameraShutterSound))), null,
-                        new AudioAttributes.Builder()
-                                .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
-                                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                                .build(), AudioSystem.newAudioSessionId());
-                completer.set(player);
+                try {
+                    MediaPlayer player = MediaPlayer.create(mContext,
+                            Uri.fromFile(new File(mContext.getResources().getString(
+                                    com.android.internal.R.string.config_cameraShutterSound))),
+                            null,
+                            new AudioAttributes.Builder()
+                                    .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+                                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                                    .build(), AudioSystem.newAudioSessionId());
+                    completer.set(player);
+                } catch (IllegalStateException e) {
+                    Log.w(TAG, "Screenshot sound initialization failed", e);
+                    completer.set(null);
+                }
             });
             return "ScreenshotController#loadCameraSound";
         });
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
index 8c8f54f..ad073c0 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
@@ -49,8 +49,8 @@
 ) : UserFileManager, CoreStartable(context) {
     companion object {
         private const val FILES = "files"
-        private const val SHARED_PREFS = "shared_prefs"
-        internal const val ID = "UserFileManager"
+        @VisibleForTesting internal const val SHARED_PREFS = "shared_prefs"
+        @VisibleForTesting internal const val ID = "UserFileManager"
     }
 
    private val broadcastReceiver = object : BroadcastReceiver() {
@@ -85,13 +85,15 @@
                 fileName
             )
         } else {
-            Environment.buildPath(
+            val secondaryFile = Environment.buildPath(
                 context.filesDir,
                 ID,
                 userId.toString(),
                 FILES,
                 fileName
             )
+            ensureParentDirExists(secondaryFile)
+            secondaryFile
         }
     }
 
@@ -114,6 +116,7 @@
             fileName
         )
 
+        ensureParentDirExists(secondaryUserDir)
         return context.getSharedPreferences(secondaryUserDir, mode)
     }
 
@@ -141,4 +144,18 @@
             }
         }
     }
+
+    /**
+     * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs
+     * recursively.
+     */
+    @VisibleForTesting
+    internal fun ensureParentDirExists(file: File) {
+        val parent = file.parentFile
+        if (!parent.exists()) {
+            if (!parent.mkdirs()) {
+                Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}")
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
index a62a152..5351024 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment
+import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import java.lang.IllegalStateException
 import javax.inject.Inject
@@ -34,7 +35,8 @@
  */
 @CentralSurfacesScope
 class StatusBarInitializer @Inject constructor(
-    private val windowController: StatusBarWindowController
+    private val windowController: StatusBarWindowController,
+    private val creationListeners: Set<@JvmSuppressWildcards OnStatusBarViewInitializedListener>,
 ) {
 
     var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null
@@ -56,6 +58,9 @@
                             statusBarFragmentComponent.phoneStatusBarViewController,
                             statusBarFragmentComponent.phoneStatusBarTransitions
                         )
+                        creationListeners.forEach { listener ->
+                            listener.onStatusBarViewInitialized(statusBarFragmentComponent)
+                        }
                     }
 
                     override fun onFragmentViewDestroyed(tag: String?, fragment: Fragment?) {
@@ -69,6 +74,17 @@
                 .commit()
     }
 
+    interface OnStatusBarViewInitializedListener {
+
+        /**
+         * The status bar view has been initialized.
+         *
+         * @param component Dagger component that is created when the status bar view is created.
+         * Can be used to retrieve dependencies from that scope, including the status bar root view.
+         */
+        fun onStatusBarViewInitialized(component: StatusBarFragmentComponent)
+    }
+
     interface OnStatusBarViewUpdatedListener {
         fun onStatusBarViewUpdated(
             statusBarView: PhoneStatusBarView,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 94341ba..3c01802 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -1909,6 +1909,8 @@
      * @param isEnabled whether the content views should be enabled for accessibility
      */
     private void updateContentAccessibilityImportanceForGuts(boolean isEnabled) {
+        updateAccessibilityImportance(isEnabled);
+
         if (mChildrenContainer != null) {
             updateChildAccessibilityImportance(mChildrenContainer, isEnabled);
         }
@@ -1924,6 +1926,15 @@
     }
 
     /**
+     * Updates whether this view is important for accessibility based on {@code isEnabled}.
+     */
+    private void updateAccessibilityImportance(boolean isEnabled) {
+        setImportantForAccessibility(isEnabled
+                ? View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+                : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+    }
+
+    /**
      * Updates whether the given childView is important for accessibility based on
      * {@code isEnabled}.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index 7c9df42..174bf4c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -25,6 +25,7 @@
 import com.android.keyguard.KeyguardSliceView;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -52,7 +53,8 @@
     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400;
     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400;
-    public static final int ANIMATION_DURATION_FOLD_TO_AOD = 600;
+    public static final int ANIMATION_DURATION_FOLD_TO_AOD =
+            AnimatableClockView.ANIMATION_DURATION_FOLD_TO_AOD;
     public static final int ANIMATION_DURATION_PULSE_APPEAR =
             KeyguardSliceView.DEFAULT_ANIM_DURATION;
     public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
index 8782be5..9070ead 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -428,7 +428,7 @@
             if (isSafetyCenterEnabled && !mAutoTracker.isAdded(mSafetySpec)) {
                 initSafetyTile();
             } else if (!isSafetyCenterEnabled && mAutoTracker.isAdded(mSafetySpec)) {
-                mHost.removeTile(CustomTile.getComponentFromSpec(mSafetySpec));
+                mHost.removeTile(mSafetySpec);
                 mHost.unmarkTileAsAutoAdded(mSafetySpec);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 53b5b0c..f4b7772 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -57,10 +57,8 @@
 import com.android.systemui.qs.QSPanelController;
 import com.android.systemui.shade.NotificationPanelView;
 import com.android.systemui.shade.NotificationPanelViewController;
-import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DisableFlagsLogger;
-import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
@@ -91,19 +89,16 @@
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final AssistManager mAssistManager;
     private final DozeServiceHost mDozeServiceHost;
-    private final SysuiStatusBarStateController mStatusBarStateController;
-    private final NotificationShadeWindowView mNotificationShadeWindowView;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     private final PowerManager mPowerManager;
     private final VibratorHelper mVibratorHelper;
     private final Optional<Vibrator> mVibratorOptional;
-    private final LightBarController mLightBarController;
     private final DisableFlagsLogger mDisableFlagsLogger;
     private final int mDisplayId;
     private final boolean mVibrateOnOpening;
     private final VibrationEffect mCameraLaunchGestureVibrationEffect;
-
+    private final SystemBarAttributesListener mSystemBarAttributesListener;
 
     private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
@@ -126,16 +121,14 @@
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             AssistManager assistManager,
             DozeServiceHost dozeServiceHost,
-            SysuiStatusBarStateController statusBarStateController,
-            NotificationShadeWindowView notificationShadeWindowView,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
             PowerManager powerManager,
             VibratorHelper vibratorHelper,
             Optional<Vibrator> vibratorOptional,
-            LightBarController lightBarController,
             DisableFlagsLogger disableFlagsLogger,
-            @DisplayId int displayId) {
+            @DisplayId int displayId,
+            SystemBarAttributesListener systemBarAttributesListener) {
 
         mCentralSurfaces = centralSurfaces;
         mContext = context;
@@ -152,20 +145,18 @@
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mAssistManager = assistManager;
         mDozeServiceHost = dozeServiceHost;
-        mStatusBarStateController = statusBarStateController;
-        mNotificationShadeWindowView = notificationShadeWindowView;
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
         mPowerManager = powerManager;
         mVibratorHelper = vibratorHelper;
         mVibratorOptional = vibratorOptional;
-        mLightBarController = lightBarController;
         mDisableFlagsLogger = disableFlagsLogger;
         mDisplayId = displayId;
 
         mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
                 mVibratorOptional, resources);
+        mSystemBarAttributesListener = systemBarAttributesListener;
     }
 
     @Override
@@ -191,7 +182,7 @@
     public void remQsTile(ComponentName tile) {
         QSPanelController qsPanelController = mCentralSurfaces.getQSPanelController();
         if (qsPanelController != null && qsPanelController.getHost() != null) {
-            qsPanelController.getHost().removeTile(tile);
+            qsPanelController.getHost().removeTileByUser(tile);
         }
     }
 
@@ -472,14 +463,18 @@
         if (displayId != mDisplayId) {
             return;
         }
-        boolean barModeChanged = mCentralSurfaces.setAppearance(appearance);
-
-        mLightBarController.onStatusBarAppearanceChanged(appearanceRegions, barModeChanged,
-                mCentralSurfaces.getBarMode(), navbarColorManagedByIme);
-
-        mCentralSurfaces.updateBubblesVisibility();
-        mStatusBarStateController.setSystemBarAttributes(
-                appearance, behavior, requestedVisibilities, packageName);
+        // SystemBarAttributesListener should __always__ be the top-level listener for system bar
+        // attributes changed.
+        mSystemBarAttributesListener.onSystemBarAttributesChanged(
+                displayId,
+                appearance,
+                appearanceRegions,
+                navbarColorManagedByIme,
+                behavior,
+                requestedVisibilities,
+                packageName,
+                letterboxDetails
+        );
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
new file mode 100644
index 0000000..70ec13b14
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
@@ -0,0 +1,227 @@
+/*
+ * 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.statusbar.phone
+
+import android.annotation.ColorInt
+import android.graphics.Color
+import android.graphics.Rect
+import android.view.InsetsFlags
+import android.view.ViewDebug
+import android.view.WindowInsetsController
+import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
+import android.view.WindowInsetsController.Appearance
+import com.android.internal.statusbar.LetterboxDetails
+import com.android.internal.util.ContrastColorUtil
+import com.android.internal.view.AppearanceRegion
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
+import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
+import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
+import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
+import java.io.PrintWriter
+import java.util.Arrays
+import javax.inject.Inject
+
+class LetterboxAppearance(
+    @Appearance val appearance: Int,
+    val appearanceRegions: Array<AppearanceRegion>
+)
+
+/**
+ * Responsible for calculating the [Appearance] and [AppearanceRegion] for the status bar when apps
+ * are letterboxed.
+ */
+@CentralSurfacesScope
+class LetterboxAppearanceCalculator
+@Inject
+constructor(
+    private val lightBarController: LightBarController,
+    private val dumpManager: DumpManager,
+) : OnStatusBarViewInitializedListener, CentralSurfacesComponent.Startable {
+
+    private var statusBarBoundsProvider: StatusBarBoundsProvider? = null
+
+    override fun start() {
+        dumpManager.registerDumpable(javaClass.simpleName) { printWriter, _ -> dump(printWriter) }
+    }
+
+    override fun stop() {
+        dumpManager.unregisterDumpable(javaClass.simpleName)
+    }
+
+    private var lastAppearance: Int? = null
+    private var lastAppearanceRegions: Array<AppearanceRegion>? = null
+    private var lastLetterboxes: Array<LetterboxDetails>? = null
+    private var lastLetterboxAppearance: LetterboxAppearance? = null
+
+    fun getLetterboxAppearance(
+        @Appearance originalAppearance: Int,
+        originalAppearanceRegions: Array<AppearanceRegion>,
+        letterboxes: Array<LetterboxDetails>
+    ): LetterboxAppearance {
+        lastAppearance = originalAppearance
+        lastAppearanceRegions = originalAppearanceRegions
+        lastLetterboxes = letterboxes
+        return getLetterboxAppearanceInternal(
+                letterboxes, originalAppearance, originalAppearanceRegions)
+            .also { lastLetterboxAppearance = it }
+    }
+
+    private fun getLetterboxAppearanceInternal(
+        letterboxes: Array<LetterboxDetails>,
+        originalAppearance: Int,
+        originalAppearanceRegions: Array<AppearanceRegion>
+    ): LetterboxAppearance {
+        if (isScrimNeeded(letterboxes)) {
+            return originalAppearanceWithScrim(originalAppearance, originalAppearanceRegions)
+        }
+        val appearance = appearanceWithoutScrim(originalAppearance)
+        val appearanceRegions = getAppearanceRegions(originalAppearanceRegions, letterboxes)
+        return LetterboxAppearance(appearance, appearanceRegions.toTypedArray())
+    }
+
+    private fun isScrimNeeded(letterboxes: Array<LetterboxDetails>): Boolean {
+        if (isOuterLetterboxMultiColored()) {
+            return true
+        }
+        return letterboxes.any { letterbox ->
+            letterbox.letterboxInnerBounds.overlapsWith(getStartSideIconBounds()) ||
+                letterbox.letterboxInnerBounds.overlapsWith(getEndSideIconsBounds())
+        }
+    }
+
+    private fun getAppearanceRegions(
+        originalAppearanceRegions: Array<AppearanceRegion>,
+        letterboxes: Array<LetterboxDetails>
+    ): List<AppearanceRegion> {
+        return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) +
+            getAllOuterAppearanceRegions(letterboxes)
+    }
+
+    private fun sanitizeAppearanceRegions(
+        originalAppearanceRegions: Array<AppearanceRegion>,
+        letterboxes: Array<LetterboxDetails>
+    ): List<AppearanceRegion> =
+        originalAppearanceRegions.map { appearanceRegion ->
+            val matchingLetterbox =
+                letterboxes.find { it.letterboxFullBounds == appearanceRegion.bounds }
+            if (matchingLetterbox == null) {
+                appearanceRegion
+            } else {
+                // When WindowManager sends appearance regions for an app, it sends them for the
+                // full bounds of its window.
+                // Here we want the bounds to be only for the inner bounds of the letterboxed app.
+                AppearanceRegion(
+                    appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds)
+            }
+        }
+
+    private fun originalAppearanceWithScrim(
+        @Appearance originalAppearance: Int,
+        originalAppearanceRegions: Array<AppearanceRegion>
+    ): LetterboxAppearance {
+        return LetterboxAppearance(
+            originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
+            originalAppearanceRegions)
+    }
+
+    @Appearance
+    private fun appearanceWithoutScrim(@Appearance originalAppearance: Int): Int =
+        originalAppearance and APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS.inv()
+
+    private fun getAllOuterAppearanceRegions(
+        letterboxes: Array<LetterboxDetails>
+    ): List<AppearanceRegion> = letterboxes.map(this::getOuterAppearanceRegions).flatten()
+
+    private fun getOuterAppearanceRegions(
+        letterboxDetails: LetterboxDetails
+    ): List<AppearanceRegion> {
+        @Appearance val outerAppearance = getOuterAppearance()
+        return getVisibleOuterBounds(letterboxDetails).map { bounds ->
+            AppearanceRegion(outerAppearance, bounds)
+        }
+    }
+
+    private fun getVisibleOuterBounds(letterboxDetails: LetterboxDetails): List<Rect> {
+        val inner = letterboxDetails.letterboxInnerBounds
+        val outer = letterboxDetails.letterboxFullBounds
+        val top = Rect(outer.left, outer.top, outer.right, inner.top)
+        val left = Rect(outer.left, outer.top, inner.left, outer.bottom)
+        val right = Rect(inner.right, outer.top, outer.right, outer.bottom)
+        val bottom = Rect(outer.left, inner.bottom, outer.right, outer.bottom)
+        return listOf(left, top, right, bottom).filter { !it.isEmpty }
+    }
+
+    @Appearance
+    private fun getOuterAppearance(): Int {
+        val backgroundColor = outerLetterboxBackgroundColor()
+        val darkAppearanceContrast =
+            ContrastColorUtil.calculateContrast(
+                lightBarController.darkAppearanceIconColor, backgroundColor)
+        val lightAppearanceContrast =
+            ContrastColorUtil.calculateContrast(
+                lightBarController.lightAppearanceIconColor, backgroundColor)
+        return if (lightAppearanceContrast > darkAppearanceContrast) {
+            WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+        } else {
+            0 // APPEARANCE_DEFAULT
+        }
+    }
+
+    @ColorInt
+    private fun outerLetterboxBackgroundColor(): Int {
+        // TODO(b/238607453): retrieve this information from WindowManager.
+        return Color.BLACK
+    }
+
+    private fun isOuterLetterboxMultiColored(): Boolean {
+        // TODO(b/238607453): retrieve this information from WindowManager.
+        return false
+    }
+
+    private fun getEndSideIconsBounds(): Rect {
+        return statusBarBoundsProvider?.visibleEndSideBounds ?: Rect()
+    }
+
+    private fun getStartSideIconBounds(): Rect {
+        return statusBarBoundsProvider?.visibleStartSideBounds ?: Rect()
+    }
+
+    override fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) {
+        statusBarBoundsProvider = component.boundsProvider
+    }
+
+    private fun Rect.overlapsWith(other: Rect): Boolean {
+        if (this.contains(other) || other.contains(this)) {
+            return false
+        }
+        return this.intersect(other)
+    }
+
+    private fun dump(printWriter: PrintWriter) {
+        printWriter.println(
+            """
+           lastAppearance: ${lastAppearance?.toAppearanceString()}
+           lastAppearanceRegion: ${Arrays.toString(lastAppearanceRegions)},
+           lastLetterboxes: ${Arrays.toString(lastLetterboxes)},
+           lastLetterboxAppearance: $lastLetterboxAppearance
+       """.trimIndent())
+    }
+}
+
+private fun Int.toAppearanceString(): String =
+    ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", this)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index 2c57328..4d14542 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -23,8 +23,8 @@
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
 
+import android.annotation.ColorInt;
 import android.content.Context;
-import android.graphics.Color;
 import android.graphics.Rect;
 import android.view.InsetsFlags;
 import android.view.ViewDebug;
@@ -63,7 +63,8 @@
     private int mStatusBarMode;
     private int mNavigationBarMode;
     private int mNavigationMode;
-    private final Color mDarkModeColor;
+    private final int mDarkIconColor;
+    private final int mLightIconColor;
 
     /**
      * Whether the navigation bar should be light factoring in already how much alpha the scrim has
@@ -94,7 +95,8 @@
             BatteryController batteryController,
             NavigationModeController navModeController,
             DumpManager dumpManager) {
-        mDarkModeColor = Color.valueOf(ctx.getColor(R.color.dark_mode_icon_color_single_tone));
+        mDarkIconColor = ctx.getColor(R.color.dark_mode_icon_color_single_tone);
+        mLightIconColor = ctx.getColor(R.color.light_mode_icon_color_single_tone);
         mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher;
         mBatteryController = batteryController;
         mBatteryController.addCallback(this);
@@ -107,6 +109,16 @@
         }
     }
 
+    @ColorInt
+    int getLightAppearanceIconColor() {
+        return mDarkIconColor;
+    }
+
+    @ColorInt
+    int getDarkAppearanceIconColor() {
+        return mLightIconColor;
+    }
+
     public void setNavigationBar(LightBarTransitionsController navigationBar) {
         mNavigationBarController = navigationBar;
         updateNavigation();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
new file mode 100644
index 0000000..a0415f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.statusbar.phone
+
+import android.view.InsetsVisibilities
+import android.view.WindowInsetsController.Appearance
+import android.view.WindowInsetsController.Behavior
+import com.android.internal.statusbar.LetterboxDetails
+import com.android.internal.view.AppearanceRegion
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
+import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Top-level listener of system attributes changed. This class is __always the first__ one to be
+ * notified about changes.
+ *
+ * It is responsible for modifying any attributes if necessary, and then notifying the other
+ * downstream listeners.
+ */
+@CentralSurfacesScope
+class SystemBarAttributesListener
+@Inject
+internal constructor(
+    private val centralSurfaces: CentralSurfaces,
+    private val featureFlags: FeatureFlags,
+    private val letterboxAppearanceCalculator: LetterboxAppearanceCalculator,
+    private val statusBarStateController: SysuiStatusBarStateController,
+    private val lightBarController: LightBarController,
+    private val dumpManager: DumpManager,
+) : CentralSurfacesComponent.Startable, StatusBarBoundsProvider.BoundsChangeListener {
+
+    private var lastLetterboxAppearance: LetterboxAppearance? = null
+    private var lastSystemBarAttributesParams: SystemBarAttributesParams? = null
+
+    override fun start() {
+        dumpManager.registerDumpable(javaClass.simpleName, this::dump)
+    }
+
+    override fun stop() {
+        dumpManager.unregisterDumpable(javaClass.simpleName)
+    }
+
+    override fun onStatusBarBoundsChanged() {
+        val params = lastSystemBarAttributesParams
+        if (params != null && shouldUseLetterboxAppearance(params.letterboxesArray)) {
+            onSystemBarAttributesChanged(
+                params.displayId,
+                params.appearance,
+                params.appearanceRegionsArray,
+                params.navbarColorManagedByIme,
+                params.behavior,
+                params.requestedVisibilities,
+                params.packageName,
+                params.letterboxesArray)
+        }
+    }
+
+    fun onSystemBarAttributesChanged(
+        displayId: Int,
+        @Appearance originalAppearance: Int,
+        originalAppearanceRegions: Array<AppearanceRegion>,
+        navbarColorManagedByIme: Boolean,
+        @Behavior behavior: Int,
+        requestedVisibilities: InsetsVisibilities,
+        packageName: String,
+        letterboxDetails: Array<LetterboxDetails>
+    ) {
+        lastSystemBarAttributesParams =
+            SystemBarAttributesParams(
+                displayId,
+                originalAppearance,
+                originalAppearanceRegions.toList(),
+                navbarColorManagedByIme,
+                behavior,
+                requestedVisibilities,
+                packageName,
+                letterboxDetails.toList())
+
+        val (appearance, appearanceRegions) =
+            modifyAppearanceIfNeeded(
+                originalAppearance, originalAppearanceRegions, letterboxDetails)
+
+        val barModeChanged = centralSurfaces.setAppearance(appearance)
+
+        lightBarController.onStatusBarAppearanceChanged(
+            appearanceRegions, barModeChanged, centralSurfaces.barMode, navbarColorManagedByIme)
+
+        centralSurfaces.updateBubblesVisibility()
+        statusBarStateController.setSystemBarAttributes(
+            appearance, behavior, requestedVisibilities, packageName)
+    }
+
+    private fun modifyAppearanceIfNeeded(
+        appearance: Int,
+        appearanceRegions: Array<AppearanceRegion>,
+        letterboxDetails: Array<LetterboxDetails>
+    ): Pair<Int, Array<AppearanceRegion>> =
+        if (shouldUseLetterboxAppearance(letterboxDetails)) {
+            val letterboxAppearance =
+                letterboxAppearanceCalculator.getLetterboxAppearance(
+                    appearance, appearanceRegions, letterboxDetails)
+            lastLetterboxAppearance = letterboxAppearance
+            Pair(letterboxAppearance.appearance, letterboxAppearance.appearanceRegions)
+        } else {
+            lastLetterboxAppearance = null
+            Pair(appearance, appearanceRegions)
+        }
+
+    private fun shouldUseLetterboxAppearance(letterboxDetails: Array<LetterboxDetails>) =
+        isLetterboxAppearanceFlagEnabled() && letterboxDetails.isNotEmpty()
+
+    private fun isLetterboxAppearanceFlagEnabled() =
+        featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)
+
+    private fun dump(printWriter: PrintWriter, strings: Array<String>) {
+        printWriter.println("lastSystemBarAttributesParams: $lastSystemBarAttributesParams")
+        printWriter.println("lastLetterboxAppearance: $lastLetterboxAppearance")
+        printWriter.println("letterbox appearance flag: ${isLetterboxAppearanceFlagEnabled()}")
+    }
+}
+
+/**
+ * Keeps track of the parameters passed in
+ * [SystemBarAttributesListener.onSystemBarAttributesChanged].
+ */
+private data class SystemBarAttributesParams(
+    val displayId: Int,
+    @Appearance val appearance: Int,
+    val appearanceRegions: List<AppearanceRegion>,
+    val navbarColorManagedByIme: Boolean,
+    @Behavior val behavior: Int,
+    val requestedVisibilities: InsetsVisibilities,
+    val packageName: String,
+    val letterboxes: List<LetterboxDetails>,
+) {
+    val letterboxesArray = letterboxes.toTypedArray()
+    val appearanceRegionsArray = appearanceRegions.toTypedArray()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
index 21e5ad5..d57e6a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
@@ -16,13 +16,28 @@
 
 package com.android.systemui.statusbar.phone.dagger;
 
+import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator;
+import com.android.systemui.statusbar.phone.SystemBarAttributesListener;
+
 import java.util.Set;
 
+import dagger.Binds;
 import dagger.Module;
+import dagger.multibindings.IntoSet;
 import dagger.multibindings.Multibinds;
 
 @Module
 interface CentralSurfacesStartableModule {
     @Multibinds
     Set<CentralSurfacesComponent.Startable> multibindStartables();
+
+    @Binds
+    @IntoSet
+    CentralSurfacesComponent.Startable letterboxAppearanceCalculator(
+            LetterboxAppearanceCalculator letterboxAppearanceCalculator);
+
+    @Binds
+    @IntoSet
+    CentralSurfacesComponent.Startable sysBarAttrsListener(
+            SystemBarAttributesListener systemBarAttributesListener);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index b607391..c4e7a8a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -46,15 +46,19 @@
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
+import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
+import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
 import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
 import com.android.systemui.statusbar.phone.StatusIconContainer;
+import com.android.systemui.statusbar.phone.SystemBarAttributesListener;
 import com.android.systemui.statusbar.phone.TapAgainView;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragmentLogger;
@@ -72,8 +76,10 @@
 
 import javax.inject.Named;
 
+import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.IntoSet;
 
 @Module(subcomponents = StatusBarFragmentComponent.class)
 public abstract class StatusBarViewModule {
@@ -248,6 +254,17 @@
         return notificationShadeWindowView.findViewById(R.id.notification_container_parent);
     }
 
+    @Binds
+    @IntoSet
+    abstract OnStatusBarViewInitializedListener statusBarInitializedListener(
+            LetterboxAppearanceCalculator letterboxAppearanceCalculator
+    );
+
+    @Binds
+    @IntoSet
+    abstract StatusBarBoundsProvider.BoundsChangeListener sysBarAttrsListenerAsBoundsListener(
+            SystemBarAttributesListener systemBarAttributesListener);
+
     /**
      * Creates a new {@link CollapsedStatusBarFragment}.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
new file mode 100644
index 0000000..bd6cf9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.statusbar.pipeline
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/**
+ * A processor that transforms raw connectivity information that we get from callbacks and turns it
+ * into a list of displayable connectivity information.
+ *
+ * This will be used for the new status bar pipeline to calculate the list of icons that should be
+ * displayed in the RHS of the status bar.
+ */
+@SysUISingleton
+class ConnectivityInfoProcessor @Inject constructor()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
new file mode 100644
index 0000000..734bd2d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.statusbar.pipeline.dagger
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import java.util.Optional
+
+@Module
+class StatusBarPipelineModule {
+    @Provides
+    @SysUISingleton
+    fun provideConnectivityInfoProcessor(
+            featureFlags: FeatureFlags,
+            processorLazy: Lazy<ConnectivityInfoProcessor>
+    ): Optional<ConnectivityInfoProcessor> {
+        return if (featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE)) {
+            Optional.of(processorLazy.get())
+        } else {
+            Optional.empty()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index 40281a1..62fc01f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -62,7 +62,6 @@
 import com.android.internal.util.LatencyTracker;
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.settingslib.users.UserCreatingDialog;
-import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.GuestResetOrExitSessionReceiver;
 import com.android.systemui.GuestResumeSessionReceiver;
@@ -167,6 +166,7 @@
     private final AtomicBoolean mGuestIsResetting;
     private final AtomicBoolean mGuestCreationScheduled;
     private FalsingManager mFalsingManager;
+    @Nullable
     private View mView;
     private String mCreateSupervisedUserPackage;
     private GlobalSettings mGlobalSettings;
@@ -572,9 +572,11 @@
 
     protected void switchToUserId(int id) {
         try {
-            mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
-                    .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mView)
-                    .setTimeout(MULTI_USER_JOURNEY_TIMEOUT));
+            if (mView != null) {
+                mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
+                        .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mView)
+                        .setTimeout(MULTI_USER_JOURNEY_TIMEOUT));
+            }
             mLatencyTracker.onActionStart(LatencyTracker.ACTION_USER_SWITCH);
             pauseRefreshUsers();
             mActivityManager.switchUser(id);
@@ -936,15 +938,17 @@
         guestCreationProgressDialog.show();
 
         // userManager.createGuest will block the thread so post is needed for the dialog to show
-        ThreadUtils.postOnMainThread(() -> {
+        mBgExecutor.execute(() -> {
             final int guestId = createGuest();
-            guestCreationProgressDialog.dismiss();
-            if (guestId == UserHandle.USER_NULL) {
-                Toast.makeText(mContext,
-                        com.android.settingslib.R.string.add_guest_failed,
-                        Toast.LENGTH_SHORT).show();
-            }
-            callback.accept(guestId);
+            mUiExecutor.execute(() -> {
+                guestCreationProgressDialog.dismiss();
+                if (guestId == UserHandle.USER_NULL) {
+                    Toast.makeText(mContext,
+                            com.android.settingslib.R.string.add_guest_failed,
+                            Toast.LENGTH_SHORT).show();
+                }
+                callback.accept(guestId);
+            });
         });
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.java b/packages/SystemUI/src/com/android/systemui/user/UserCreator.java
deleted file mode 100644
index 0686071c..0000000
--- a/packages/SystemUI/src/com/android/systemui/user/UserCreator.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2020 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.user;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.UserManager;
-
-import com.android.internal.util.UserIcons;
-import com.android.settingslib.users.UserCreatingDialog;
-import com.android.settingslib.utils.ThreadUtils;
-
-import java.util.function.Consumer;
-
-import javax.inject.Inject;
-
-/**
- * A class to do the user creation process. It shows a progress dialog, and manages the user
- * creation
- */
-public class UserCreator {
-
-    private final Context mContext;
-    private final UserManager mUserManager;
-
-    @Inject
-    public UserCreator(Context context, UserManager userManager) {
-        mContext = context;
-        mUserManager = userManager;
-    }
-
-    /**
-     * Shows a progress dialog then starts the user creation process on the main thread.
-     *
-     * @param successCallback is called when the user creation is successful.
-     * @param errorCallback   is called when userManager.createUser returns null.
-     *                        (Exceptions are not handled by this class)
-     */
-    public void createUser(String userName, Drawable userIcon, Consumer<UserInfo> successCallback,
-            Runnable errorCallback) {
-
-        Dialog userCreationProgressDialog = new UserCreatingDialog(mContext);
-        userCreationProgressDialog.show();
-
-        // userManager.createUser will block the thread so post is needed for the dialog to show
-        ThreadUtils.postOnMainThread(() -> {
-            UserInfo user =
-                    mUserManager.createUser(userName, UserManager.USER_TYPE_FULL_SECONDARY, 0);
-            if (user == null) {
-                // Couldn't create user for some reason
-                userCreationProgressDialog.dismiss();
-                errorCallback.run();
-                return;
-            }
-
-            Drawable newUserIcon = userIcon;
-            Resources res = mContext.getResources();
-            if (newUserIcon == null) {
-                newUserIcon = UserIcons.getDefaultUserIcon(res, user.id, false);
-            }
-            mUserManager.setUserIcon(
-                    user.id, UserIcons.convertToBitmapAtUserIconSize(res, newUserIcon));
-
-            userCreationProgressDialog.dismiss();
-            successCallback.accept(user);
-        });
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
new file mode 100644
index 0000000..dcbbe74
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 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.user
+
+import android.app.Dialog
+import android.content.Context
+import android.content.pm.UserInfo
+import android.graphics.drawable.Drawable
+import android.os.UserManager
+import com.android.internal.util.UserIcons
+import com.android.settingslib.users.UserCreatingDialog
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * A class to do the user creation process. It shows a progress dialog, and manages the user
+ * creation
+ */
+class UserCreator @Inject constructor(
+    private val context: Context,
+    private val userManager: UserManager,
+    @Main private val mainExecutor: Executor,
+    @Background private val bgExecutor: Executor
+) {
+    /**
+     * Shows a progress dialog then starts the user creation process on the main thread.
+     *
+     * @param successCallback is called when the user creation is successful.
+     * @param errorCallback is called when userManager.createUser returns null.
+     * (Exceptions are not handled by this class)
+     */
+    fun createUser(
+        userName: String?,
+        userIcon: Drawable?,
+        successCallback: Consumer<UserInfo?>,
+       errorCallback: Runnable
+    ) {
+        val userCreationProgressDialog: Dialog = UserCreatingDialog(context)
+        userCreationProgressDialog.show()
+
+        // userManager.createUser will block the thread so post is needed for the dialog to show
+        bgExecutor.execute {
+            val user = userManager.createUser(userName, UserManager.USER_TYPE_FULL_SECONDARY, 0)
+            mainExecutor.execute main@{
+                if (user == null) {
+                    // Couldn't create user for some reason
+                    userCreationProgressDialog.dismiss()
+                    errorCallback.run()
+                    return@main
+                }
+                bgExecutor.execute {
+                    var newUserIcon = userIcon
+                    val res = context.resources
+                    if (newUserIcon == null) {
+                        newUserIcon = UserIcons.getDefaultUserIcon(res, user.id, false)
+                    }
+                    userManager.setUserIcon(
+                        user.id, UserIcons.convertToBitmapAtUserIconSize(res, newUserIcon))
+                }
+                userCreationProgressDialog.dismiss()
+                successCallback.accept(user)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 5db2cf4..8b9a1e0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -51,6 +51,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.plugins.ClockPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index 8717a0e..6c6f0ac 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -43,6 +43,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ClockPlugin;
+import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.statusbar.StatusBarState;
 
 import org.junit.Before;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index 70025230..4dcaa7c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -89,28 +89,6 @@
     }
 
     @Test
-    public void timeFormatUpdateNotifiesClockSwitchController() {
-        mController.onViewAttached();
-
-        verify(mKeyguardUpdateMonitor).registerCallback(
-                mKeyguardUpdateMonitorCallbackCaptor.capture());
-
-        mKeyguardUpdateMonitorCallbackCaptor.getValue().onTimeFormatChanged("");
-        verify(mKeyguardClockSwitchController).refreshFormat();
-    }
-
-    @Test
-    public void userChangeNotifiesClockSwitchController() {
-        mController.onViewAttached();
-
-        verify(mKeyguardUpdateMonitor).registerCallback(
-                mKeyguardUpdateMonitorCallbackCaptor.capture());
-
-        mKeyguardUpdateMonitorCallbackCaptor.getValue().onUserSwitchComplete(0);
-        verify(mKeyguardClockSwitchController).refreshFormat();
-    }
-
-    @Test
     public void setTranslationYExcludingMedia_forwardsCallToView() {
         float translationY = 123f;
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/FontInterpolatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/keyguard/FontInterpolatorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt
index 95fa3b9..f01da2d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/FontInterpolatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.keyguard
+package com.android.systemui.animation
 
 import android.graphics.Paint
 import android.graphics.fonts.Font
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
similarity index 99%
rename from packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
index 3322414..ed0cd7e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.keyguard
+package com.android.systemui.animation
 
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
@@ -26,18 +26,17 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.ceil
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-
-import kotlin.math.ceil
+import org.mockito.Mockito.`when`
 
 private val PAINT = TextPaint().apply {
     textSize = 32f
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/TextInterpolatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextInterpolatorTest.kt
similarity index 99%
rename from packages/SystemUI/tests/src/com/android/keyguard/TextInterpolatorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/animation/TextInterpolatorTest.kt
index 603cf3b..2a183bd 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/TextInterpolatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextInterpolatorTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.keyguard
+package com.android.systemui.animation
 
 import android.graphics.Bitmap
 import android.graphics.Canvas
@@ -31,10 +31,10 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
 import java.io.File
 import kotlin.math.ceil
+import org.junit.Test
+import org.junit.runner.RunWith
 
 private const val TEXT = "Hello, World."
 private const val BIDI_TEXT = "abc\u05D0\u05D1\u05D2"
@@ -323,4 +323,4 @@
         Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { draw(Canvas(it)) }!!
 
 private fun TextInterpolator.toBitmap(width: Int, height: Int) =
-        Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { draw(Canvas(it)) }
\ No newline at end of file
+        Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { draw(Canvas(it)) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt
new file mode 100644
index 0000000..1e4a9e4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.controls.management
+
+import android.content.ComponentName
+import android.content.res.Resources
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import androidx.test.filters.SmallTest
+import com.android.settingslib.core.lifecycle.Lifecycle
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class AppAdapterTest : SysuiTestCase() {
+    private val fakeSystemClock = FakeSystemClock()
+    private val backgroundExecutor = FakeExecutor(fakeSystemClock)
+    private val uiExecutor = FakeExecutor(fakeSystemClock)
+    @Mock lateinit var lifecycle: Lifecycle
+    @Mock lateinit var controlsListingController: ControlsListingController
+    @Mock lateinit var layoutInflater: LayoutInflater
+    @Mock lateinit var onAppSelected: (ComponentName?) -> Unit
+    @Mock lateinit var favoritesRenderer: FavoritesRenderer
+    val resources: Resources = context.resources
+    lateinit var adapter: AppAdapter
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        adapter = AppAdapter(backgroundExecutor,
+            uiExecutor,
+            lifecycle,
+            controlsListingController,
+            layoutInflater,
+            onAppSelected,
+            favoritesRenderer,
+            resources)
+    }
+
+    @Test
+    fun testOnServicesUpdated_nullLoadLabel() {
+        val captor = ArgumentCaptor
+            .forClass(ControlsListingController.ControlsListingCallback::class.java)
+        val controlsServiceInfo = mock(ControlsServiceInfo::class.java)
+        val serviceInfo = listOf(controlsServiceInfo)
+        `when`(controlsServiceInfo.loadLabel()).thenReturn(null)
+        verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
+
+        captor.value.onServicesUpdated(serviceInfo)
+        backgroundExecutor.runAllReady()
+        uiExecutor.runAllReady()
+
+        assertThat(adapter.itemCount).isEqualTo(serviceInfo.size)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java
index 5a4bb86..df506b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java
@@ -34,12 +34,12 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.AnimatableClockController;
-import com.android.keyguard.AnimatableClockView;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.settingslib.Utils;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.statusbar.policy.BatteryController;
 
 import org.junit.After;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 664af75..32c66d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -32,8 +32,6 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 import android.view.LayoutInflater;
@@ -42,36 +40,23 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.R;
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.animation.ShadeInterpolation;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSFragmentComponent;
-import com.android.systemui.qs.external.CustomTileStatePersister;
-import com.android.systemui.qs.external.TileLifecycleManager;
 import com.android.systemui.qs.external.TileServiceRequestController;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.tileimpl.QSFactoryImpl;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.phone.AutoTileManager;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
-import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.animation.UniqueObjectHostView;
-import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -79,8 +64,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Optional;
-
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
@@ -125,34 +108,11 @@
         mFragments.dispatchResume();
         processAllMessages();
 
-        QSTileHost host =
-                new QSTileHost(
-                        mContext,
-                        mock(StatusBarIconController.class),
-                        mock(QSFactoryImpl.class),
-                        new Handler(),
-                        Looper.myLooper(),
-                        mock(PluginManager.class),
-                        mock(TunerService.class),
-                        () -> mock(AutoTileManager.class),
-                        mock(DumpManager.class),
-                        mock(BroadcastDispatcher.class),
-                        Optional.of(mock(CentralSurfaces.class)),
-                        mock(QSLogger.class),
-                        mock(UiEventLogger.class),
-                        mock(UserTracker.class),
-                        mock(SecureSettings.class),
-                        mock(CustomTileStatePersister.class),
-                        mTileServiceRequestControllerBuilder,
-                        mock(TileLifecycleManager.Factory.class));
-
         qs.setListening(true);
         processAllMessages();
 
         qs.setListening(false);
         processAllMessages();
-        host.destroy();
-        processAllMessages();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 8cf3fe2..7dbc561 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -32,12 +32,11 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.database.ContentObserver;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.testing.TestableLooper.RunWithLooper;
 import android.view.View;
 
 import androidx.annotation.Nullable;
@@ -48,7 +47,6 @@
 import com.android.internal.util.CollectionUtils;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.ActivityStarter;
@@ -68,8 +66,10 @@
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -81,18 +81,19 @@
 import java.io.StringWriter;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.Executor;
 
 import javax.inject.Provider;
 
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-@RunWithLooper(setAsMainLooper = true)
 public class QSTileHostTest extends SysuiTestCase {
 
     private static String MOCK_STATE_STRING = "MockState";
     private static ComponentName CUSTOM_TILE =
             ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS");
     private static final String CUSTOM_TILE_SPEC = CustomTile.toSpec(CUSTOM_TILE);
+    private static final String SETTING = QSTileHost.TILES_SETTING;
 
     @Mock
     private StatusBarIconController mIconController;
@@ -107,8 +108,6 @@
     @Mock
     private DumpManager mDumpManager;
     @Mock
-    private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock
     private QSTile.State mMockState;
     @Mock
     private CentralSurfaces mCentralSurfaces;
@@ -132,31 +131,47 @@
     @Mock
     private TileLifecycleManager mTileLifecycleManager;
 
-    private Handler mHandler;
-    private TestableLooper mLooper;
+    private FakeExecutor mMainExecutor;
+
     private QSTileHost mQSTileHost;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mLooper = TestableLooper.get(this);
-        mHandler = new Handler(mLooper.getLooper());
+        mMainExecutor = new FakeExecutor(new FakeSystemClock());
+
         when(mTileServiceRequestControllerBuilder.create(any()))
                 .thenReturn(mTileServiceRequestController);
         when(mTileLifecycleManagerFactory.create(any(Intent.class), any(UserHandle.class)))
                 .thenReturn(mTileLifecycleManager);
 
         mSecureSettings = new FakeSettings();
-        mSecureSettings.putStringForUser(
-                QSTileHost.TILES_SETTING, "", "", false, mUserTracker.getUserId(), false);
-        mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mHandler,
-                mLooper.getLooper(), mPluginManager, mTunerService, mAutoTiles, mDumpManager,
-                mBroadcastDispatcher, mCentralSurfaces, mQSLogger, mUiEventLogger, mUserTracker,
-                mSecureSettings, mCustomTileStatePersister, mTileServiceRequestControllerBuilder,
-                mTileLifecycleManagerFactory);
+        saveSetting("");
+        mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mMainExecutor,
+                mPluginManager, mTunerService, mAutoTiles, mDumpManager, mCentralSurfaces,
+                mQSLogger, mUiEventLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister,
+                mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory);
+
+        mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) {
+            @Override
+            public void onChange(boolean selfChange) {
+                super.onChange(selfChange);
+                mMainExecutor.execute(() -> mQSTileHost.onTuningChanged(SETTING, getSetting()));
+                mMainExecutor.runAllReady();
+            }
+        }, mUserTracker.getUserId());
         setUpTileFactory();
     }
 
+    private void saveSetting(String value) {
+        mSecureSettings.putStringForUser(
+                SETTING, value, "", false, mUserTracker.getUserId(), false);
+    }
+
+    private String getSetting() {
+        return mSecureSettings.getStringForUser(SETTING, mUserTracker.getUserId());
+    }
+
     private void setUpTileFactory() {
         when(mMockState.toString()).thenReturn(MOCK_STATE_STRING);
         // Only create this kind of tiles
@@ -173,6 +188,10 @@
                         return new NotAvailableTile(mQSTileHost);
                     } else if (CUSTOM_TILE_SPEC.equals(spec)) {
                         return mCustomTile;
+                    } else if ("internet".equals(spec)
+                            || "wifi".equals(spec)
+                            || "cell".equals(spec)) {
+                        return new TestTile1(mQSTileHost);
                     } else {
                         return null;
                     }
@@ -196,14 +215,14 @@
     public void testInvalidSpecUsesDefault() {
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles, "spec1,spec2");
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "not-valid");
+        saveSetting("not-valid");
 
         assertEquals(2, mQSTileHost.getTiles().size());
     }
 
     @Test
     public void testRemoveWifiAndCellularWithoutInternet() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "wifi, spec1, cell, spec2");
+        saveSetting("wifi, spec1, cell, spec2");
 
         assertEquals("internet", mQSTileHost.mTileSpecs.get(0));
         assertEquals("spec1", mQSTileHost.mTileSpecs.get(1));
@@ -212,7 +231,7 @@
 
     @Test
     public void testRemoveWifiAndCellularWithInternet() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "wifi, spec1, cell, spec2, internet");
+        saveSetting("wifi, spec1, cell, spec2, internet");
 
         assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
         assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
@@ -221,7 +240,7 @@
 
     @Test
     public void testRemoveWifiWithoutInternet() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1, wifi, spec2");
+        saveSetting("spec1, wifi, spec2");
 
         assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
         assertEquals("internet", mQSTileHost.mTileSpecs.get(1));
@@ -230,7 +249,7 @@
 
     @Test
     public void testRemoveCellWithInternet() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1, spec2, cell, internet");
+        saveSetting("spec1, spec2, cell, internet");
 
         assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
         assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
@@ -239,7 +258,7 @@
 
     @Test
     public void testNoWifiNoCellularNoInternet() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1,spec2");
+        saveSetting("spec1,spec2");
 
         assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
         assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
@@ -249,7 +268,7 @@
     public void testSpecWithInvalidDoesNotUseDefault() {
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles, "spec1,spec2");
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec2,not-valid");
+        saveSetting("spec2,not-valid");
 
         assertEquals(1, mQSTileHost.getTiles().size());
         QSTile element = CollectionUtils.firstOrNull(mQSTileHost.getTiles());
@@ -258,7 +277,7 @@
 
     @Test
     public void testDump() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1,spec2");
+        saveSetting("spec1,spec2");
         StringWriter w = new StringWriter();
         PrintWriter pw = new PrintWriter(w);
         mQSTileHost.dump(pw, new String[]{});
@@ -274,7 +293,7 @@
     public void testDefault() {
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles_default, "spec1");
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "default");
+        saveSetting("default");
         assertEquals(1, mQSTileHost.getTiles().size());
         QSTile element = CollectionUtils.firstOrNull(mQSTileHost.getTiles());
         assertTrue(element instanceof TestTile1);
@@ -285,7 +304,7 @@
     public void testNoRepeatedSpecs_addTile() {
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles, "spec1,spec2");
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1,spec2");
+        saveSetting("spec1,spec2");
 
         mQSTileHost.addTile("spec1");
 
@@ -298,9 +317,10 @@
     public void testAddTileAtValidPosition() {
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles, "spec1,spec3");
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1,spec3");
+        saveSetting("spec1,spec3");
 
         mQSTileHost.addTile("spec2", 1);
+        mMainExecutor.runAllReady();
 
         assertEquals(3, mQSTileHost.mTileSpecs.size());
         assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
@@ -312,9 +332,10 @@
     public void testAddTileAtInvalidPositionAddsToEnd() {
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles, "spec1,spec3");
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1,spec3");
+        saveSetting("spec1,spec3");
 
         mQSTileHost.addTile("spec2", 100);
+        mMainExecutor.runAllReady();
 
         assertEquals(3, mQSTileHost.mTileSpecs.size());
         assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
@@ -326,9 +347,10 @@
     public void testAddTileAtEnd() {
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles, "spec1,spec3");
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1,spec3");
+        saveSetting("spec1,spec3");
 
         mQSTileHost.addTile("spec2", QSTileHost.POSITION_AT_END);
+        mMainExecutor.runAllReady();
 
         assertEquals(3, mQSTileHost.mTileSpecs.size());
         assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
@@ -338,9 +360,10 @@
 
     @Test
     public void testNoRepeatedSpecs_customTile() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, CUSTOM_TILE_SPEC);
+        saveSetting(CUSTOM_TILE_SPEC);
 
         mQSTileHost.addTile(CUSTOM_TILE, /* end */ false);
+        mMainExecutor.runAllReady();
 
         assertEquals(1, mQSTileHost.mTileSpecs.size());
         assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
@@ -348,9 +371,10 @@
 
     @Test
     public void testAddedAtBeginningOnDefault_customTile() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1"); // seed
+        saveSetting("spec1"); // seed
 
         mQSTileHost.addTile(CUSTOM_TILE);
+        mMainExecutor.runAllReady();
 
         assertEquals(2, mQSTileHost.mTileSpecs.size());
         assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
@@ -358,9 +382,10 @@
 
     @Test
     public void testAddedAtBeginning_customTile() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1"); // seed
+        saveSetting("spec1"); // seed
 
         mQSTileHost.addTile(CUSTOM_TILE, /* end */ false);
+        mMainExecutor.runAllReady();
 
         assertEquals(2, mQSTileHost.mTileSpecs.size());
         assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
@@ -368,9 +393,10 @@
 
     @Test
     public void testAddedAtEnd_customTile() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1"); // seed
+        saveSetting("spec1"); // seed
 
         mQSTileHost.addTile(CUSTOM_TILE, /* end */ true);
+        mMainExecutor.runAllReady();
 
         assertEquals(2, mQSTileHost.mTileSpecs.size());
         assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(1));
@@ -409,13 +435,13 @@
 
     @Test
     public void testNotAvailableTile_specNotNull() {
-        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "na");
+        saveSetting("na");
         verify(mQSLogger, never()).logTileDestroyed(isNull(), anyString());
     }
 
     @Test
     public void testCustomTileRemoved_stateDeleted() {
-        mQSTileHost.changeTiles(List.of(CUSTOM_TILE_SPEC), List.of());
+        mQSTileHost.changeTilesByUser(List.of(CUSTOM_TILE_SPEC), List.of());
 
         verify(mCustomTileStatePersister)
                 .removeState(new TileServiceKey(CUSTOM_TILE, mQSTileHost.getUserId()));
@@ -423,29 +449,99 @@
 
     @Test
     public void testRemoveTiles() {
-        List<String> tiles = List.of("spec1", "spec2", "spec3");
-        mQSTileHost.saveTilesToSettings(tiles);
+        saveSetting("spec1,spec2,spec3");
 
         mQSTileHost.removeTiles(List.of("spec1", "spec2"));
 
+        mMainExecutor.runAllReady();
         assertEquals(List.of("spec3"), mQSTileHost.mTileSpecs);
     }
 
+    @Test
+    public void testTilesRemovedInQuickSuccession() {
+        saveSetting("spec1,spec2,spec3");
+        mQSTileHost.removeTile("spec1");
+        mQSTileHost.removeTile("spec3");
+
+        mMainExecutor.runAllReady();
+        assertEquals(List.of("spec2"), mQSTileHost.mTileSpecs);
+        assertEquals("spec2", getSetting());
+    }
+
+    @Test
+    public void testAddTileInMainThread() {
+        saveSetting("spec1,spec2");
+
+        mQSTileHost.addTile("spec3");
+        assertEquals(List.of("spec1", "spec2"), mQSTileHost.mTileSpecs);
+
+        mMainExecutor.runAllReady();
+        assertEquals(List.of("spec1", "spec2", "spec3"), mQSTileHost.mTileSpecs);
+    }
+
+    @Test
+    public void testRemoveTileInMainThread() {
+        saveSetting("spec1,spec2");
+
+        mQSTileHost.removeTile("spec1");
+        assertEquals(List.of("spec1", "spec2"), mQSTileHost.mTileSpecs);
+
+        mMainExecutor.runAllReady();
+        assertEquals(List.of("spec2"), mQSTileHost.mTileSpecs);
+    }
+
+    @Test
+    public void testRemoveTilesInMainThread() {
+        saveSetting("spec1,spec2,spec3");
+
+        mQSTileHost.removeTiles(List.of("spec3", "spec1"));
+        assertEquals(List.of("spec1", "spec2", "spec3"), mQSTileHost.mTileSpecs);
+
+        mMainExecutor.runAllReady();
+        assertEquals(List.of("spec2"), mQSTileHost.mTileSpecs);
+    }
+
+    @Test
+    public void testRemoveTileByUserInMainThread() {
+        saveSetting("spec1," + CUSTOM_TILE_SPEC);
+
+        mQSTileHost.removeTileByUser(CUSTOM_TILE);
+        assertEquals(List.of("spec1", CUSTOM_TILE_SPEC), mQSTileHost.mTileSpecs);
+
+        mMainExecutor.runAllReady();
+        assertEquals(List.of("spec1"), mQSTileHost.mTileSpecs);
+    }
+
+    @Test
+    public void testNonValidTileNotStoredInSettings() {
+        saveSetting("spec1,not-valid");
+
+        assertEquals(List.of("spec1"), mQSTileHost.mTileSpecs);
+        assertEquals("spec1", getSetting());
+    }
+
+    @Test
+    public void testNotAvailableTileNotStoredInSettings() {
+        saveSetting("spec1,na");
+
+        assertEquals(List.of("spec1"), mQSTileHost.mTileSpecs);
+        assertEquals("spec1", getSetting());
+    }
+
     private class TestQSTileHost extends QSTileHost {
         TestQSTileHost(Context context, StatusBarIconController iconController,
-                QSFactory defaultFactory, Handler mainHandler, Looper bgLooper,
+                QSFactory defaultFactory, Executor mainExecutor,
                 PluginManager pluginManager, TunerService tunerService,
                 Provider<AutoTileManager> autoTiles, DumpManager dumpManager,
-                BroadcastDispatcher broadcastDispatcher, CentralSurfaces centralSurfaces,
-                QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker,
-                SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister,
+                CentralSurfaces centralSurfaces, QSLogger qsLogger, UiEventLogger uiEventLogger,
+                UserTracker userTracker, SecureSettings secureSettings,
+                CustomTileStatePersister customTileStatePersister,
                 TileServiceRequestController.Builder tileServiceRequestControllerBuilder,
                 TileLifecycleManager.Factory tileLifecycleManagerFactory) {
-            super(context, iconController, defaultFactory, mainHandler, bgLooper, pluginManager,
-                    tunerService, autoTiles, dumpManager, broadcastDispatcher,
-                    Optional.of(centralSurfaces), qsLogger, uiEventLogger, userTracker,
-                    secureSettings, customTileStatePersister, tileServiceRequestControllerBuilder,
-                    tileLifecycleManagerFactory);
+            super(context, iconController, defaultFactory, mainExecutor, pluginManager,
+                    tunerService, autoTiles, dumpManager, Optional.of(centralSurfaces), qsLogger,
+                    uiEventLogger, userTracker, secureSettings, customTileStatePersister,
+                    tileServiceRequestControllerBuilder, tileLifecycleManagerFactory);
         }
 
         @Override
@@ -455,25 +551,16 @@
         @Override
         public void onPluginDisconnected(QSFactory plugin) {
         }
-
-        @Override
-        void saveTilesToSettings(List<String> tileSpecs) {
-            super.saveTilesToSettings(tileSpecs);
-            // After tiles are changed, make sure to call onTuningChanged with the new setting if it
-            // changed
-            String specs = mSecureSettings.getStringForUser(
-                    QSTileHost.TILES_SETTING, mUserTracker.getUserId());
-            onTuningChanged(TILES_SETTING, specs);
-        }
     }
 
+
     private class TestTile extends QSTileImpl<QSTile.State> {
 
         protected TestTile(QSHost host) {
             super(
                     host,
-                    mLooper.getLooper(),
-                    new Handler(mLooper.getLooper()),
+                    mock(Looper.class),
+                    mock(Handler.class),
                     new FalsingManagerFake(),
                     mock(MetricsLogger.class),
                     mock(StatusBarStateController.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java
index 3d53062..d42cbe3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java
@@ -55,6 +55,6 @@
     @Test
     public void testResetNotifiesHost() {
         mTileAdapter.resetTileSpecs(Collections.emptyList());
-        verify(mQSTileHost).changeTiles(any(), any());
+        verify(mQSTileHost).changeTilesByUser(any(), any());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
index 6b7e5b93..471ddfd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
@@ -29,6 +29,7 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.service.quicksettings.IQSTileService;
@@ -65,6 +66,7 @@
 
 import java.util.ArrayList;
 import java.util.Optional;
+import java.util.concurrent.Executor;
 
 import javax.inject.Provider;
 
@@ -130,17 +132,16 @@
                 .thenReturn(mTileLifecycleManager);
 
         Provider<Handler> provider = () -> new Handler(mTestableLooper.getLooper());
+        Executor executor = new HandlerExecutor(provider.get());
 
         QSTileHost host = new QSTileHost(mContext,
                 mStatusBarIconController,
                 mQSFactory,
-                provider.get(),
-                mTestableLooper.getLooper(),
+                executor,
                 mPluginManager,
                 mTunerService,
                 () -> mAutoTileManager,
                 mDumpManager,
-                mock(BroadcastDispatcher.class),
                 Optional.of(mCentralSurfaces),
                 mQSLogger,
                 mUiEventLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
index 73226fa..6d9b01e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -62,6 +63,14 @@
             broadcastDispatcher, backgroundExecutor)
     }
 
+    @After
+    fun end() {
+        val dir = Environment.buildPath(
+            context.filesDir,
+            UserFileManagerImpl.ID)
+        dir.deleteRecursively()
+    }
+
     @Test
     fun testGetFile() {
         assertThat(userFileManager.getFile(TEST_FILE_NAME, 0).path)
@@ -72,8 +81,19 @@
 
     @Test
     fun testGetSharedPreferences() {
+        val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11)
+        val secondaryUserDir = Environment.buildPath(
+            context.filesDir,
+            UserFileManagerImpl.ID,
+            "11",
+            UserFileManagerImpl.SHARED_PREFS,
+            TEST_FILE_NAME
+        )
+
+        assertThat(secondarySharedPref).isNotNull()
+        assertThat(secondaryUserDir.exists())
         assertThat(userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 0))
-            .isNotEqualTo(userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11))
+            .isNotEqualTo(secondarySharedPref)
     }
 
     @Test
@@ -115,6 +135,19 @@
         verify(userManager).aliveUsers
         assertThat(secondaryUserDir.exists()).isFalse()
         assertThat(file.exists()).isFalse()
-        dir.deleteRecursively()
+    }
+
+    @Test
+    fun testEnsureParentDirExists() {
+        val file = Environment.buildPath(
+            context.filesDir,
+            UserFileManagerImpl.ID,
+            "11",
+            "files",
+            TEST_FILE_NAME
+        )
+        assertThat(file.parentFile.exists()).isFalse()
+        userFileManager.ensureParentDirExists(file)
+        assertThat(file.parentFile.exists()).isTrue()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index aaa2357..d61989f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -15,14 +15,17 @@
  */
 package com.android.systemui.shared.clocks
 
-import org.mockito.Mockito.`when` as whenever
-import android.content.Context
 import android.content.ContentResolver
+import android.content.Context
 import android.graphics.drawable.Drawable
 import android.os.Handler
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.Clock
+import com.android.systemui.plugins.ClockId
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.plugins.ClockProviderPlugin
 import com.android.systemui.plugins.PluginListener
 import com.android.systemui.shared.plugins.PluginManager
 import com.android.systemui.util.mockito.argumentCaptor
@@ -35,6 +38,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
 @RunWith(AndroidTestingRunner::class)
@@ -45,9 +49,11 @@
     @Mock private lateinit var mockContext: Context
     @Mock private lateinit var mockPluginManager: PluginManager
     @Mock private lateinit var mockClock: Clock
+    @Mock private lateinit var mockDefaultClock: Clock
     @Mock private lateinit var mockThumbnail: Drawable
     @Mock private lateinit var mockHandler: Handler
     @Mock private lateinit var mockContentResolver: ContentResolver
+    private lateinit var fakeDefaultProvider: FakeClockPlugin
     private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
     private lateinit var registry: ClockRegistry
 
@@ -79,42 +85,52 @@
             name: String,
             create: () -> Clock = ::failFactory,
             getThumbnail: () -> Drawable? = ::failThumbnail
-        ) {
+        ): FakeClockPlugin {
             metadata.add(ClockMetadata(id, name))
             createCallbacks[id] = create
             thumbnailCallbacks[id] = getThumbnail
+            return this
         }
     }
 
     @Before
     fun setUp() {
+        fakeDefaultProvider = FakeClockPlugin()
+            .addClock(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME, { mockDefaultClock }, { mockThumbnail })
         whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
 
         val captor = argumentCaptor<PluginListener<ClockProviderPlugin>>()
-        registry = object : ClockRegistry(mockContext, mockPluginManager, mockHandler) {
+        registry = object : ClockRegistry(
+            mockContext,
+            mockPluginManager,
+            mockHandler,
+            fakeDefaultProvider
+        ) {
             override var currentClockId: ClockId
                 get() = settingValue
                 set(value) { settingValue = value }
         }
-        verify(mockPluginManager).addPluginListener(captor.capture(),
-            eq(ClockProviderPlugin::class.java))
+
+        verify(mockPluginManager)
+            .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java))
         pluginListener = captor.value
     }
 
     @Test
     fun pluginRegistration_CorrectState() {
         val plugin1 = FakeClockPlugin()
-        plugin1.addClock("clock_1", "clock 1")
-        plugin1.addClock("clock_2", "clock 2")
+            .addClock("clock_1", "clock 1")
+            .addClock("clock_2", "clock 2")
 
         val plugin2 = FakeClockPlugin()
-        plugin2.addClock("clock_3", "clock 3")
-        plugin2.addClock("clock_4", "clock 4")
+            .addClock("clock_3", "clock 3")
+            .addClock("clock_4", "clock 4")
 
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
         val list = registry.getClocks()
         assertEquals(list, listOf(
+            ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
             ClockMetadata("clock_1", "clock 1"),
             ClockMetadata("clock_2", "clock 2"),
             ClockMetadata("clock_3", "clock 3"),
@@ -123,19 +139,26 @@
     }
 
     @Test
+    fun noPlugins_createDefaultClock() {
+        val clock = registry.createCurrentClock()
+        assertEquals(clock, mockDefaultClock)
+    }
+
+    @Test
     fun clockIdConflict_ErrorWithoutCrash() {
         val plugin1 = FakeClockPlugin()
-        plugin1.addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail })
-        plugin1.addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail })
+            .addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail })
+            .addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail })
 
         val plugin2 = FakeClockPlugin()
-        plugin2.addClock("clock_1", "clock 1")
-        plugin2.addClock("clock_2", "clock 2")
+            .addClock("clock_1", "clock 1")
+            .addClock("clock_2", "clock 2")
 
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
         val list = registry.getClocks()
         assertEquals(list, listOf(
+            ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
             ClockMetadata("clock_1", "clock 1"),
             ClockMetadata("clock_2", "clock 2")
         ))
@@ -149,13 +172,13 @@
     @Test
     fun createCurrentClock_pluginConnected() {
         val plugin1 = FakeClockPlugin()
-        plugin1.addClock("clock_1", "clock 1")
-        plugin1.addClock("clock_2", "clock 2")
+            .addClock("clock_1", "clock 1")
+            .addClock("clock_2", "clock 2")
 
         settingValue = "clock_3"
         val plugin2 = FakeClockPlugin()
-        plugin2.addClock("clock_3", "clock 3", { mockClock })
-        plugin2.addClock("clock_4", "clock 4")
+            .addClock("clock_3", "clock 3", { mockClock })
+            .addClock("clock_4", "clock 4")
 
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
@@ -167,32 +190,32 @@
     @Test
     fun createDefaultClock_pluginDisconnected() {
         val plugin1 = FakeClockPlugin()
-        plugin1.addClock(DEFAULT_CLOCK_ID, "default", { mockClock })
-        plugin1.addClock("clock_2", "clock 2")
+            .addClock("clock_1", "clock 1")
+            .addClock("clock_2", "clock 2")
 
         settingValue = "clock_3"
         val plugin2 = FakeClockPlugin()
-        plugin2.addClock("clock_3", "clock 3")
-        plugin2.addClock("clock_4", "clock 4")
+            .addClock("clock_3", "clock 3")
+            .addClock("clock_4", "clock 4")
 
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
         pluginListener.onPluginDisconnected(plugin2)
 
         val clock = registry.createCurrentClock()
-        assertEquals(clock, mockClock)
+        assertEquals(clock, mockDefaultClock)
     }
 
     @Test
     fun pluginRemoved_clockChanged() {
         val plugin1 = FakeClockPlugin()
-        plugin1.addClock("clock_1", "clock 1")
-        plugin1.addClock("clock_2", "clock 2")
+            .addClock("clock_1", "clock 1")
+            .addClock("clock_2", "clock 2")
 
         settingValue = "clock_3"
         val plugin2 = FakeClockPlugin()
-        plugin2.addClock("clock_3", "clock 3", { mockClock })
-        plugin2.addClock("clock_4", "clock 4")
+            .addClock("clock_3", "clock 3", { mockClock })
+            .addClock("clock_4", "clock 4")
 
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
new file mode 100644
index 0000000..7869448
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.shared.clocks
+
+import android.content.res.Resources
+import android.graphics.drawable.Drawable
+import android.testing.AndroidTestingRunner
+import android.view.LayoutInflater
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class DefaultClockProviderTest : SysuiTestCase() {
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Mock private lateinit var mockClockView: AnimatableClockView
+    @Mock private lateinit var layoutInflater: LayoutInflater
+    @Mock private lateinit var mockClockThumbnail: Drawable
+    @Mock private lateinit var resources: Resources
+    private lateinit var provider: DefaultClockProvider
+
+    @Before
+    fun setUp() {
+        whenever(layoutInflater.inflate(R.layout.clock_default_small, null))
+            .thenReturn(mockClockView)
+        whenever(layoutInflater.inflate(R.layout.clock_default_large, null))
+            .thenReturn(mockClockView)
+        whenever(resources.getDrawable(R.drawable.clock_default_thumbnail, null))
+            .thenReturn(mockClockThumbnail)
+
+        provider = DefaultClockProvider(layoutInflater, resources)
+    }
+
+    @Test
+    fun providedClocks_matchesFactory() {
+        // All providers need to provide clocks & thumbnails for exposed clocks
+        for (metadata in provider.getClocks()) {
+            assertNotNull(provider.createClock(metadata.clockId))
+            assertNotNull(provider.getClockThumbnail(metadata.clockId))
+        }
+    }
+
+    @Test
+    fun defaultClock_alwaysProvided() {
+        // Default clock provider must always provide the default clock
+        val clock = provider.createClock(DEFAULT_CLOCK_ID)
+        assertNotNull(clock)
+        assertEquals(clock.smallClock, mockClockView)
+        assertEquals(clock.largeClock, mockClockView)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
index 214ba16..64d0256 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
@@ -98,7 +98,7 @@
 
     @Test
     fun testPrepareDialogForApp_onlyDefaultChannel() {
-        group.channels = listOf(channelDefault)
+        group.addChannel(channelDefault)
 
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
                 setOf(channelDefault), appIcon, clickListener)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
index 371119c..4ccbc6d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
@@ -490,7 +490,7 @@
         mAutoTileManager.init();
         when(mAutoAddTracker.isAdded(TEST_CUSTOM_SAFETY_SPEC)).thenReturn(true);
         mAutoTileManager.mSafetyCallback.onSafetyCenterEnableChanged(false);
-        verify(mQsTileHost, times(1)).removeTile(safetyComponent);
+        verify(mQsTileHost, times(1)).removeTile(TEST_CUSTOM_SAFETY_SPEC);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index d79f336..7b7a48b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -22,26 +22,28 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.app.StatusBarManager;
 import android.os.PowerManager;
 import android.os.Vibrator;
 import android.testing.AndroidTestingRunner;
+import android.view.InsetsVisibilities;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.testing.FakeMetricsLogger;
+import com.android.internal.statusbar.LetterboxDetails;
+import com.android.internal.view.AppearanceRegion;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.shade.NotificationPanelViewController;
-import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DisableFlagsLogger;
-import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -60,6 +62,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
+
     @Mock private CentralSurfaces mCentralSurfaces;
     @Mock private ShadeController mShadeController;
     @Mock private CommandQueue mCommandQueue;
@@ -74,14 +77,12 @@
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock private AssistManager mAssistManager;
     @Mock private DozeServiceHost mDozeServiceHost;
-    @Mock private StatusBarStateControllerImpl mStatusBarStateController;
-    @Mock private NotificationShadeWindowView mNotificationShadeWindowView;
     @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     @Mock private PowerManager mPowerManager;
     @Mock private VibratorHelper mVibratorHelper;
     @Mock private Vibrator mVibrator;
-    @Mock private LightBarController mLightBarController;
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
+    @Mock private SystemBarAttributesListener mSystemBarAttributesListener;
 
     CentralSurfacesCommandQueueCallbacks mSbcqCallbacks;
 
@@ -106,16 +107,14 @@
                 mStatusBarKeyguardViewManager,
                 mAssistManager,
                 mDozeServiceHost,
-                mStatusBarStateController,
-                mNotificationShadeWindowView,
                 mNotificationStackScrollLayoutController,
                 mStatusBarHideIconsForBouncerManager,
                 mPowerManager,
                 mVibratorHelper,
                 Optional.of(mVibrator),
-                mLightBarController,
                 new DisableFlagsLogger(),
-                DEFAULT_DISPLAY);
+                DEFAULT_DISPLAY,
+                mSystemBarAttributesListener);
 
         when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
         when(mRemoteInputQuickSettingsDisabler.adjustDisableFlags(anyInt()))
@@ -170,5 +169,59 @@
         verify(mDozeServiceHost).setAlwaysOnSuppressed(false);
     }
 
+    @Test
+    public void onSystemBarAttributesChanged_forwardsToSysBarAttrsListener() {
+        int displayId = DEFAULT_DISPLAY;
+        int appearance = 123;
+        AppearanceRegion[] appearanceRegions = new AppearanceRegion[]{};
+        boolean navbarColorManagedByIme = true;
+        int behavior = 456;
+        InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        String packageName = "test package name";
+        LetterboxDetails[] letterboxDetails = new LetterboxDetails[]{};
 
+        mSbcqCallbacks.onSystemBarAttributesChanged(
+                displayId,
+                appearance,
+                appearanceRegions,
+                navbarColorManagedByIme,
+                behavior,
+                requestedVisibilities,
+                packageName,
+                letterboxDetails);
+
+        verify(mSystemBarAttributesListener).onSystemBarAttributesChanged(
+                displayId,
+                appearance,
+                appearanceRegions,
+                navbarColorManagedByIme,
+                behavior,
+                requestedVisibilities,
+                packageName,
+                letterboxDetails
+        );
+    }
+
+    @Test
+    public void onSystemBarAttributesChanged_differentDisplayId_doesNotForwardToAttrsListener() {
+        int appearance = 123;
+        AppearanceRegion[] appearanceRegions = new AppearanceRegion[]{};
+        boolean navbarColorManagedByIme = true;
+        int behavior = 456;
+        InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        String packageName = "test package name";
+        LetterboxDetails[] letterboxDetails = new LetterboxDetails[]{};
+
+        mSbcqCallbacks.onSystemBarAttributesChanged(
+                DEFAULT_DISPLAY + 1,
+                appearance,
+                appearanceRegions,
+                navbarColorManagedByIme,
+                behavior,
+                requestedVisibilities,
+                packageName,
+                letterboxDetails);
+
+        verifyZeroInteractions(mSystemBarAttributesListener);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
new file mode 100644
index 0000000..6540135
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.statusbar.phone
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.WindowInsetsController
+import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
+import androidx.test.filters.SmallTest
+import com.android.internal.statusbar.LetterboxDetails
+import com.android.internal.view.AppearanceRegion
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
+import com.google.common.truth.Expect
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import com.android.systemui.dump.DumpManager
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
+
+    companion object {
+        private const val DEFAULT_APPEARANCE = 0
+        private const val TEST_APPEARANCE = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+        private val TEST_APPEARANCE_REGION_BOUNDS = Rect(0, 0, 20, 100)
+        private val TEST_APPEARANCE_REGION =
+            AppearanceRegion(TEST_APPEARANCE, TEST_APPEARANCE_REGION_BOUNDS)
+        private val TEST_APPEARANCE_REGIONS = arrayOf(TEST_APPEARANCE_REGION)
+        private val TEST_WINDOW_BOUNDS = Rect(0, 0, 500, 500)
+    }
+
+    @get:Rule var expect = Expect.create()
+
+    @Mock private lateinit var lightBarController: LightBarController
+    @Mock private lateinit var statusBarBoundsProvider: StatusBarBoundsProvider
+    @Mock private lateinit var statusBarFragmentComponent: StatusBarFragmentComponent
+    @Mock private lateinit var dumpManager: DumpManager
+
+    private lateinit var calculator: LetterboxAppearanceCalculator
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(statusBarFragmentComponent.boundsProvider).thenReturn(statusBarBoundsProvider)
+        calculator = LetterboxAppearanceCalculator(lightBarController, dumpManager)
+        calculator.onStatusBarViewInitialized(statusBarFragmentComponent)
+    }
+
+    @Test
+    fun getLetterboxAppearance_overlapStartSide_returnsOriginalWithScrim() {
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
+        val letterbox = letterboxWithInnerBounds(Rect(50, 50, 150, 150))
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
+
+        expect
+            .that(letterboxAppearance.appearance)
+            .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
+        expect.that(letterboxAppearance.appearanceRegions).isEqualTo(TEST_APPEARANCE_REGIONS)
+    }
+
+    @Test
+    fun getLetterboxAppearance_overlapEndSide_returnsOriginalWithScrim() {
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
+        val letterbox = letterboxWithInnerBounds(Rect(150, 50, 250, 150))
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
+
+        expect
+            .that(letterboxAppearance.appearance)
+            .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
+        expect.that(letterboxAppearance.appearanceRegions).isEqualTo(TEST_APPEARANCE_REGIONS)
+    }
+
+    @Test
+    fun getLetterboxAppearance_noOverlap_returnsAppearanceWithoutScrim() {
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
+        val letterbox = letterboxWithInnerBounds(Rect(101, 0, 199, 100))
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
+
+        assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
+    }
+
+    @Test
+    fun getLetterboxAppearance_letterboxContainsStartSide_returnsAppearanceWithoutScrim() {
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
+        val letterbox = letterboxWithInnerBounds(Rect(0, 0, 101, 101))
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
+
+        assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
+    }
+
+    @Test
+    fun getLetterboxAppearance_letterboxContainsEndSide_returnsAppearanceWithoutScrim() {
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
+        val letterbox = letterboxWithInnerBounds(Rect(199, 0, 301, 101))
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
+
+        assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
+    }
+
+    @Test
+    fun getLetterboxAppearance_letterboxContainsEntireStatusBar_returnsAppearanceWithoutScrim() {
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
+        val letterbox = letterboxWithInnerBounds(Rect(0, 0, 300, 100))
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
+
+        assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
+    }
+
+    @Test
+    fun getLetterboxAppearance_returnsAdaptedAppearanceRegions_basedOnLetterboxInnerBounds() {
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 0, 0))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(0, 0, 0, 0))
+        val letterbox = letterboxWithInnerBounds(Rect(150, 0, 300, 800))
+        val letterboxRegion = TEST_APPEARANCE_REGION.copy(bounds = letterbox.letterboxFullBounds)
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, arrayOf(letterboxRegion), arrayOf(letterbox))
+
+        val letterboxAdaptedRegion = letterboxRegion.copy(bounds = letterbox.letterboxInnerBounds)
+        assertThat(letterboxAppearance.appearanceRegions.toList()).contains(letterboxAdaptedRegion)
+        assertThat(letterboxAppearance.appearanceRegions.toList()).doesNotContain(letterboxRegion)
+    }
+
+    @Test
+    fun getLetterboxAppearance_returnsDefaultAppearanceRegions_basedOnLetterboxOuterBounds() {
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 0, 0))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(0, 0, 0, 0))
+        val letterbox =
+            letterboxWithBounds(
+                innerBounds = Rect(left = 25, top = 0, right = 75, bottom = 100),
+                fullBounds = Rect(left = 0, top = 0, right = 100, bottom = 100))
+        val letterboxRegion = TEST_APPEARANCE_REGION.copy(bounds = letterbox.letterboxFullBounds)
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, arrayOf(letterboxRegion), arrayOf(letterbox))
+
+        val outerRegions =
+            listOf(
+                AppearanceRegion(
+                    DEFAULT_APPEARANCE, Rect(left = 0, top = 0, right = 25, bottom = 100)),
+                AppearanceRegion(
+                    DEFAULT_APPEARANCE, Rect(left = 75, top = 0, right = 100, bottom = 100)),
+            )
+        assertThat(letterboxAppearance.appearanceRegions.toList())
+            .containsAtLeastElementsIn(outerRegions)
+    }
+
+    private fun letterboxWithBounds(innerBounds: Rect, fullBounds: Rect) =
+        LetterboxDetails(innerBounds, fullBounds, TEST_APPEARANCE)
+
+    private fun letterboxWithInnerBounds(innerBounds: Rect) =
+        letterboxWithBounds(innerBounds, fullBounds = TEST_WINDOW_BOUNDS)
+}
+
+private fun AppearanceRegion.copy(appearance: Int = this.appearance, bounds: Rect = this.bounds) =
+    AppearanceRegion(appearance, bounds)
+
+private fun Rect(left: Int, top: Int, right: Int, bottom: Int) = Rect(left, top, right, bottom)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt
new file mode 100644
index 0000000..fa7b259
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt
@@ -0,0 +1,250 @@
+package com.android.systemui.statusbar.phone
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.InsetsVisibilities
+import android.view.WindowInsetsController
+import android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
+import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+import android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS
+import android.view.WindowInsetsController.Appearance
+import androidx.test.filters.SmallTest
+import com.android.internal.statusbar.LetterboxDetails
+import com.android.internal.view.AppearanceRegion
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SystemBarAttributesListenerTest : SysuiTestCase() {
+
+    @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var lightBarController: LightBarController
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock private lateinit var letterboxAppearanceCalculator: LetterboxAppearanceCalculator
+    @Mock private lateinit var featureFlags: FeatureFlags
+    @Mock private lateinit var centralSurfaces: CentralSurfaces
+
+    private lateinit var sysBarAttrsListener: SystemBarAttributesListener
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(
+                letterboxAppearanceCalculator.getLetterboxAppearance(
+                    anyInt(), anyObject(), anyObject()))
+            .thenReturn(TEST_LETTERBOX_APPEARANCE)
+
+        sysBarAttrsListener =
+            SystemBarAttributesListener(
+                centralSurfaces,
+                featureFlags,
+                letterboxAppearanceCalculator,
+                statusBarStateController,
+                lightBarController,
+                dumpManager)
+    }
+
+    @Test
+    fun onSysBarAttrsChanged_forwardsAppearanceToCentralSurfaces() {
+        val appearance = APPEARANCE_LIGHT_STATUS_BARS or APPEARANCE_LIGHT_NAVIGATION_BARS
+
+        changeSysBarAttrs(appearance)
+
+        verify(centralSurfaces).setAppearance(appearance)
+    }
+
+    @Test
+    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToCentralSurfaces() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
+
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS)
+
+        verify(centralSurfaces).setAppearance(TEST_LETTERBOX_APPEARANCE.appearance)
+    }
+
+    @Test
+    fun onSysBarAttrsChanged_flagTrue_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
+
+        changeSysBarAttrs(TEST_APPEARANCE, arrayOf<LetterboxDetails>())
+
+        verify(centralSurfaces).setAppearance(TEST_APPEARANCE)
+    }
+
+    @Test
+    fun onSysBarAttrsChanged_forwardsAppearanceToStatusBarStateController() {
+        changeSysBarAttrs(TEST_APPEARANCE)
+
+        verify(statusBarStateController)
+            .setSystemBarAttributes(eq(TEST_APPEARANCE), anyInt(), any(), any())
+    }
+
+    @Test
+    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToStatusBarStateCtrl() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
+
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS)
+
+        verify(statusBarStateController)
+            .setSystemBarAttributes(
+                eq(TEST_LETTERBOX_APPEARANCE.appearance), anyInt(), any(), any())
+    }
+
+    @Test
+    fun onSysBarAttrsChanged_forwardsAppearanceToLightBarController() {
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS)
+
+        verify(lightBarController)
+            .onStatusBarAppearanceChanged(
+                eq(TEST_APPEARANCE_REGIONS), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToLightBarController() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
+
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
+
+        verify(lightBarController)
+            .onStatusBarAppearanceChanged(
+                eq(TEST_LETTERBOX_APPEARANCE.appearanceRegions),
+                anyBoolean(),
+                anyInt(),
+                anyBoolean())
+    }
+
+    @Test
+    fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToStatusBarStateController() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
+        reset(centralSurfaces, lightBarController, statusBarStateController)
+
+        sysBarAttrsListener.onStatusBarBoundsChanged()
+
+        verify(statusBarStateController)
+            .setSystemBarAttributes(
+                eq(TEST_LETTERBOX_APPEARANCE.appearance), anyInt(), any(), any())
+    }
+
+    @Test
+    fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToLightBarController() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
+        reset(centralSurfaces, lightBarController, statusBarStateController)
+
+        sysBarAttrsListener.onStatusBarBoundsChanged()
+
+        verify(lightBarController)
+            .onStatusBarAppearanceChanged(
+                eq(TEST_LETTERBOX_APPEARANCE.appearanceRegions),
+                anyBoolean(),
+                anyInt(),
+                anyBoolean())
+    }
+
+    @Test
+    fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToCentralSurfaces() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
+        reset(centralSurfaces, lightBarController, statusBarStateController)
+
+        sysBarAttrsListener.onStatusBarBoundsChanged()
+
+        verify(centralSurfaces).setAppearance(TEST_LETTERBOX_APPEARANCE.appearance)
+    }
+
+    @Test
+    fun onStatusBarBoundsChanged_previousCallEmptyLetterbox_doesNothing() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf())
+        reset(centralSurfaces, lightBarController, statusBarStateController)
+
+        sysBarAttrsListener.onStatusBarBoundsChanged()
+
+        verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController)
+    }
+
+    @Test
+    fun onStatusBarBoundsChanged_flagFalse_doesNothing() {
+        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(false)
+        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
+        reset(centralSurfaces, lightBarController, statusBarStateController)
+
+        sysBarAttrsListener.onStatusBarBoundsChanged()
+
+        verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController)
+    }
+
+    private fun changeSysBarAttrs(@Appearance appearance: Int) {
+        changeSysBarAttrs(appearance, arrayOf<LetterboxDetails>())
+    }
+
+    private fun changeSysBarAttrs(
+        @Appearance appearance: Int,
+        letterboxDetails: Array<LetterboxDetails>
+    ) {
+        changeSysBarAttrs(appearance, arrayOf(), letterboxDetails)
+    }
+
+    private fun changeSysBarAttrs(
+        @Appearance appearance: Int,
+        appearanceRegions: Array<AppearanceRegion>
+    ) {
+        changeSysBarAttrs(appearance, appearanceRegions, arrayOf())
+    }
+
+    private fun changeSysBarAttrs(
+        @Appearance appearance: Int,
+        appearanceRegions: Array<AppearanceRegion>,
+        letterboxDetails: Array<LetterboxDetails>
+    ) {
+        sysBarAttrsListener.onSystemBarAttributesChanged(
+            Display.DEFAULT_DISPLAY,
+            appearance,
+            appearanceRegions,
+            /* navbarColorManagedByIme= */ false,
+            WindowInsetsController.BEHAVIOR_DEFAULT,
+            InsetsVisibilities(),
+            "package name",
+            letterboxDetails)
+    }
+
+    companion object {
+        private const val TEST_APPEARANCE =
+            APPEARANCE_LIGHT_STATUS_BARS or APPEARANCE_LIGHT_NAVIGATION_BARS
+        private val TEST_APPEARANCE_REGION = AppearanceRegion(TEST_APPEARANCE, Rect(0, 0, 150, 300))
+        private val TEST_APPEARANCE_REGIONS = arrayOf(TEST_APPEARANCE_REGION)
+        private val TEST_LETTERBOX_DETAILS =
+            arrayOf(
+                LetterboxDetails(
+                    /* letterboxInnerBounds= */ Rect(0, 0, 0, 0),
+                    /* letterboxFullBounds= */ Rect(0, 0, 0, 0),
+                    /* appAppearance= */ 0))
+        private val TEST_LETTERBOX_APPEARANCE =
+            LetterboxAppearance(/* appearance= */ APPEARANCE_LOW_PROFILE_BARS, arrayOf())
+    }
+}
+
+private fun <T> anyObject(): T {
+    return Mockito.anyObject<T>()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
index 09d7c03..359a780 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
@@ -77,6 +77,7 @@
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -269,6 +270,8 @@
         `when`(userManager.createGuest(any())).thenReturn(guestInfo)
 
         userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null)
+        bgExecutor.runAllReady()
+        uiExecutor.runAllReady()
         testableLooper.processAllMessages()
         verify(interactionJankMonitor).begin(any())
         verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH)
@@ -294,6 +297,8 @@
         `when`(userManager.createGuest(any())).thenReturn(guestInfo)
 
         userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, dialogShower)
+        bgExecutor.runAllReady()
+        uiExecutor.runAllReady()
         testableLooper.processAllMessages()
         verify(dialogShower).dismiss()
     }
@@ -584,4 +589,24 @@
         broadcastReceiverCaptor.value.onReceive(context, intent)
         verify(cb).onUserSwitched()
     }
+
+    @Test
+    fun onUserItemClicked_guest_runsOnBgThread() {
+        val dialogShower = mock(UserSwitchDialogController.DialogShower::class.java)
+        val guestUserRecord = UserSwitcherController.UserRecord(
+            null,
+            picture,
+            true /* guest */,
+            false /* current */,
+            false /* isAddUser */,
+            false /* isRestricted */,
+            true /* isSwitchToEnabled */,
+            false /* isAddSupervisedUser */)
+
+        userSwitcherController.onUserListItemClicked(guestUserRecord, dialogShower)
+        assertTrue(bgExecutor.numPending() > 0)
+        verify(userManager, never()).createGuest(context)
+        bgExecutor.runAllReady()
+        verify(userManager).createGuest(context)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/UserCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/UserCreatorTest.kt
new file mode 100644
index 0000000..a85ae7df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/UserCreatorTest.kt
@@ -0,0 +1,73 @@
+package com.android.systemui.user
+
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.os.UserManager
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class UserCreatorTest : SysuiTestCase() {
+    companion object {
+        const val USER_NAME = "abc"
+    }
+
+    @Mock
+    private lateinit var userCreator: UserCreator
+    @Mock
+    private lateinit var userManager: UserManager
+    private lateinit var mainExecutor: FakeExecutor
+    private lateinit var bgExecutor: FakeExecutor
+    private lateinit var user: UserInfo
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mainExecutor = FakeExecutor(FakeSystemClock())
+        bgExecutor = FakeExecutor(FakeSystemClock())
+        userCreator = UserCreator(context, userManager, mainExecutor, bgExecutor)
+        user = Mockito.mock(UserInfo::class.java)
+        Mockito.`when`(userManager.createUser(USER_NAME, UserManager.USER_TYPE_FULL_SECONDARY, 0))
+            .thenReturn(user)
+    }
+
+    @Test
+    fun testCreateUser_threadingOrder() {
+        val successCallback = Mockito.mock(Consumer::class.java)
+        val errorCallback = Mockito.mock(Runnable::class.java)
+
+        userCreator.createUser(
+            USER_NAME,
+            null,
+            successCallback as Consumer<UserInfo?>,
+            errorCallback)
+
+        verify(userManager, never()).createUser(USER_NAME, UserManager.USER_TYPE_FULL_SECONDARY, 0)
+        bgExecutor.runAllReady()
+        verify(successCallback, never()).accept(user)
+        mainExecutor.runAllReady()
+        verify(userManager, never()).setUserIcon(anyInt(), any(Bitmap::class.java))
+        bgExecutor.runAllReady()
+
+        verify(userManager).createUser(USER_NAME, UserManager.USER_TYPE_FULL_SECONDARY, 0)
+        verify(userManager).setUserIcon(anyInt(), any(Bitmap::class.java))
+        verify(successCallback).accept(user)
+    }
+}
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index 6076eb1..7a09ce7 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -2790,13 +2790,6 @@
         if (isOnSystemDeviceIdleAllowlist(uid)) {
             return REASON_SYSTEM_ALLOW_LISTED;
         }
-        if (isOnDeviceIdleAllowlist(uid)) {
-            return REASON_ALLOWLISTED_PACKAGE;
-        }
-        final ActivityManagerInternal am = mInjector.getActivityManagerInternal();
-        if (am.isAssociatedCompanionApp(UserHandle.getUserId(uid), uid)) {
-            return REASON_COMPANION_DEVICE_MANAGER;
-        }
         if (UserManager.isDeviceInDemoMode(mContext)) {
             return REASON_DEVICE_DEMO_MODE;
         }
@@ -2805,6 +2798,7 @@
                 .hasUserRestriction(UserManager.DISALLOW_APPS_CONTROL, userId)) {
             return REASON_DISALLOW_APPS_CONTROL;
         }
+        final ActivityManagerInternal am = mInjector.getActivityManagerInternal();
         if (am.isDeviceOwner(uid)) {
             return REASON_DEVICE_OWNER;
         }
@@ -2822,14 +2816,9 @@
             final AppOpsManager appOpsManager = mInjector.getAppOpsManager();
             final PackageManagerInternal pm = mInjector.getPackageManagerInternal();
             final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal();
+            // Check each packages to see if any of them is in the "fixed" exemption cases.
             for (String pkg : packages) {
-                if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN,
-                        uid, pkg) == AppOpsManager.MODE_ALLOWED) {
-                    return REASON_OP_ACTIVATE_VPN;
-                } else if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN,
-                        uid, pkg) == AppOpsManager.MODE_ALLOWED) {
-                    return REASON_OP_ACTIVATE_PLATFORM_VPN;
-                } else if (isSystemModule(pkg)) {
+                if (isSystemModule(pkg)) {
                     return REASON_SYSTEM_MODULE;
                 } else if (isCarrierApp(pkg)) {
                     return REASON_CARRIER_PRIVILEGED_APP;
@@ -2843,6 +2832,16 @@
                     return REASON_ACTIVE_DEVICE_ADMIN;
                 }
             }
+            // Loop the packages again, and check the user-configurable exemptions.
+            for (String pkg : packages) {
+                if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN,
+                        uid, pkg) == AppOpsManager.MODE_ALLOWED) {
+                    return REASON_OP_ACTIVATE_VPN;
+                } else if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN,
+                        uid, pkg) == AppOpsManager.MODE_ALLOWED) {
+                    return REASON_OP_ACTIVATE_PLATFORM_VPN;
+                }
+            }
         }
         if (isRoleHeldByUid(RoleManager.ROLE_DIALER, uid)) {
             return REASON_ROLE_DIALER;
@@ -2850,6 +2849,12 @@
         if (isRoleHeldByUid(RoleManager.ROLE_EMERGENCY, uid)) {
             return REASON_ROLE_EMERGENCY;
         }
+        if (isOnDeviceIdleAllowlist(uid)) {
+            return REASON_ALLOWLISTED_PACKAGE;
+        }
+        if (am.isAssociatedCompanionApp(UserHandle.getUserId(uid), uid)) {
+            return REASON_COMPANION_DEVICE_MANAGER;
+        }
         return REASON_DENIED;
     }
 
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index cd5960f..1def72b 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -46,6 +46,8 @@
 import android.util.Pair;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -103,10 +105,6 @@
             AudioDeviceInfo.TYPE_BLE_BROADCAST
     };
 
-    private static final int[] WIRELESS_SPEAKER_TYPES = {
-            AudioDeviceInfo.TYPE_BLE_SPEAKER,
-    };
-
     // Spatializer state machine
     private static final int STATE_UNINITIALIZED = 0;
     private static final int STATE_NOT_SUPPORTED = 1;
@@ -166,6 +164,7 @@
      * List of devices where Spatial Audio is possible. Each device can be enabled or disabled
      * (== user choice to use or not)
      */
+    @GuardedBy("this")
     private final ArrayList<SADeviceState> mSADevices = new ArrayList<>(0);
 
     //------------------------------------------------------
@@ -520,30 +519,30 @@
      *                    set to true if the device is added to the list, otherwise, if already
      *                    present, the setting is left untouched.
      */
+    @GuardedBy("this")
     private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada,
             boolean forceEnable) {
         if (!isDeviceCompatibleWithSpatializationModes(ada)) {
             return;
         }
         loglogi("addCompatibleAudioDevice: dev=" + ada);
-        boolean isInList = false;
+        final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
         SADeviceState deviceUpdated = null; // non-null on update.
-
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                isInList = true;
-                if (forceEnable) {
-                    deviceState.mEnabled = true;
-                    deviceUpdated = deviceState;
-                }
-                break;
+        if (deviceState != null) {
+            if (forceEnable && !deviceState.mEnabled) {
+                deviceUpdated = deviceState;
+                deviceUpdated.mEnabled = true;
             }
-        }
-        if (!isInList) {
-            final SADeviceState deviceState = new SADeviceState(ada.getType(), ada.getAddress());
-            deviceState.mEnabled = true;
-            mSADevices.add(deviceState);
-            deviceUpdated = deviceState;
+        } else {
+            // When adding, force the device type to be a canonical one.
+            final int canonicalDeviceType = getCanonicalDeviceType(ada.getType());
+            if (canonicalDeviceType == AudioDeviceInfo.TYPE_UNKNOWN) {
+                Log.e(TAG, "addCompatibleAudioDevice with incompatible AudioDeviceAttributes "
+                        + ada);
+                return;
+            }
+            deviceUpdated = new SADeviceState(canonicalDeviceType, ada.getAddress());
+            mSADevices.add(deviceUpdated);
         }
         if (deviceUpdated != null) {
             onRoutingUpdated();
@@ -574,90 +573,95 @@
 
     synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
         loglogi("removeCompatibleAudioDevice: dev=" + ada);
-        SADeviceState deviceUpdated = null; // non-null on update.
 
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                deviceState.mEnabled = false;
-                deviceUpdated = deviceState;
-                break;
-            }
-        }
-        if (deviceUpdated != null) {
+        final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        if (deviceState != null && deviceState.mEnabled) {
+            deviceState.mEnabled = false;
             onRoutingUpdated();
             mAudioService.persistSpatialAudioDeviceSettings();
-            logDeviceState(deviceUpdated, "removeCompatibleAudioDevice");
+            logDeviceState(deviceState, "removeCompatibleAudioDevice");
         }
     }
 
     /**
+     * Returns a possibly aliased device type which is used
+     * for spatial audio settings (or TYPE_UNKNOWN  if it doesn't exist).
+     */
+    private static @AudioDeviceInfo.AudioDeviceType int getCanonicalDeviceType(int deviceType) {
+        if (isWireless(deviceType)) return deviceType;
+
+        final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE);
+        if (spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL) {
+            return AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
+        } else if (spatMode == SpatializationMode.SPATIALIZER_BINAURAL) {
+            return AudioDeviceInfo.TYPE_WIRED_HEADPHONES;
+        }
+        return AudioDeviceInfo.TYPE_UNKNOWN;
+    }
+
+    /**
+     * Returns the Spatial Audio device state for an audio device attributes
+     * or null if it does not exist.
+     */
+    @GuardedBy("this")
+    @Nullable
+    private SADeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada) {
+        final int deviceType = ada.getType();
+        final boolean isWireless = isWireless(deviceType);
+        final int canonicalDeviceType = getCanonicalDeviceType(deviceType);
+
+        for (SADeviceState deviceState : mSADevices) {
+            if (deviceState.mDeviceType == canonicalDeviceType
+                    && (!isWireless || ada.getAddress().equals(deviceState.mDeviceAddress))) {
+                return deviceState;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Return if Spatial Audio is enabled and available for the given device
      * @param ada
      * @return a pair of boolean, 1/ enabled? 2/ available?
      */
     private synchronized Pair<Boolean, Boolean> evaluateState(AudioDeviceAttributes ada) {
-        // if not a wireless device, this value will be overwritten to map the type
-        // to TYPE_BUILTIN_SPEAKER or TYPE_WIRED_HEADPHONES
-        @AudioDeviceInfo.AudioDeviceType int deviceType = ada.getType();
-
-        // if not a wireless device: find if media device is in the speaker, wired headphones
-        if (!isWireless(deviceType)) {
-            // is the device type capable of doing SA?
-            if (!mSACapableDeviceTypes.contains(deviceType)) {
-                Log.i(TAG, "Device incompatible with Spatial Audio dev:" + ada);
-                return new Pair<>(false, false);
-            }
-            // what spatialization mode to use for this device?
-            final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE);
-            if (spatMode == Integer.MIN_VALUE) {
-                // error case, device not found
-                Log.e(TAG, "no spatialization mode found for device type:" + deviceType);
-                return new Pair<>(false, false);
-            }
-            // map the spatialization mode to the SPEAKER or HEADPHONES device
-            if (spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL) {
-                deviceType = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
-            } else {
-                deviceType = AudioDeviceInfo.TYPE_WIRED_HEADPHONES;
-            }
-        } else { // wireless device
-            if (isWirelessSpeaker(deviceType) && !mTransauralSupported) {
-                Log.i(TAG, "Device incompatible with Spatial Audio (no transaural) dev:"
-                        + ada);
-                return new Pair<>(false, false);
-            }
-            if (!mBinauralSupported) {
-                Log.i(TAG, "Device incompatible with Spatial Audio (no binaural) dev:"
-                        + ada);
-                return new Pair<>(false, false);
-            }
+        final @AudioDeviceInfo.AudioDeviceType int deviceType = ada.getType();
+        // is the device type capable of doing SA?
+        if (!mSACapableDeviceTypes.contains(deviceType)) {
+            Log.i(TAG, "Device incompatible with Spatial Audio dev:" + ada);
+            return new Pair<>(false, false);
         }
-
-        boolean enabled = false;
-        boolean available = false;
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                available = true;
-                enabled = deviceState.mEnabled;
-                break;
-            }
+        // what spatialization mode to use for this device?
+        final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE);
+        if (spatMode == Integer.MIN_VALUE) {
+            // error case, device not found
+            Log.e(TAG, "no spatialization mode found for device type:" + deviceType);
+            return new Pair<>(false, false);
         }
-        return new Pair<>(enabled, available);
+        final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        if (deviceState == null) {
+            // no matching device state?
+            Log.i(TAG, "no spatialization device state found for Spatial Audio device:" + ada);
+            return new Pair<>(false, false);
+        }
+        // found the matching device state.
+        return new Pair<>(deviceState.mEnabled, true /* available */);
     }
 
     private synchronized void addWirelessDeviceIfNew(@NonNull AudioDeviceAttributes ada) {
         if (!isDeviceCompatibleWithSpatializationModes(ada)) {
             return;
         }
-        boolean knownDevice = false;
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                knownDevice = true;
-                break;
+        if (findDeviceStateForAudioDeviceAttributes(ada) == null) {
+            // wireless device types should be canonical, but we translate to be sure.
+            final int canonicalDeviceType = getCanonicalDeviceType((ada.getType()));
+            if (canonicalDeviceType == AudioDeviceInfo.TYPE_UNKNOWN) {
+                Log.e(TAG, "addWirelessDeviceIfNew with incompatible AudioDeviceAttributes "
+                        + ada);
+                return;
             }
-        }
-        if (!knownDevice) {
-            final SADeviceState deviceState = new SADeviceState(ada.getType(), ada.getAddress());
+            final SADeviceState deviceState =
+                    new SADeviceState(canonicalDeviceType, ada.getAddress());
             mSADevices.add(deviceState);
             mAudioService.persistSpatialAudioDeviceSettings();
             logDeviceState(deviceState, "addWirelessDeviceIfNew"); // may be updated later.
@@ -699,12 +703,7 @@
         if (ada.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) {
             return false;
         }
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                return true;
-            }
-        }
-        return false;
+        return findDeviceStateForAudioDeviceAttributes(ada) != null;
     }
 
     private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes,
@@ -1086,20 +1085,18 @@
             Log.v(TAG, "no headtracking support, ignoring setHeadTrackerEnabled to " + enabled
                     + " for " + ada);
         }
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                if (!deviceState.mHasHeadTracker) {
-                    Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled
-                            + " device:" + ada + " on a device without headtracker");
-                    return;
-                }
-                Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
-                deviceState.mHeadTrackerEnabled = enabled;
-                mAudioService.persistSpatialAudioDeviceSettings();
-                logDeviceState(deviceState, "setHeadTrackerEnabled");
-                break;
-            }
+        final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        if (deviceState == null) return;
+        if (!deviceState.mHasHeadTracker) {
+            Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled
+                    + " device:" + ada + " on a device without headtracker");
+            return;
         }
+        Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
+        deviceState.mHeadTrackerEnabled = enabled;
+        mAudioService.persistSpatialAudioDeviceSettings();
+        logDeviceState(deviceState, "setHeadTrackerEnabled");
+
         // check current routing to see if it affects the headtracking mode
         if (ROUTING_DEVICES[0].getType() == ada.getType()
                 && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) {
@@ -1113,12 +1110,8 @@
             Log.v(TAG, "no headtracking support, hasHeadTracker always false for " + ada);
             return false;
         }
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                return deviceState.mHasHeadTracker;
-            }
-        }
-        return false;
+        final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        return deviceState != null && deviceState.mHasHeadTracker;
     }
 
     /**
@@ -1131,15 +1124,14 @@
             Log.v(TAG, "no headtracking support, setHasHeadTracker always false for " + ada);
             return false;
         }
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                if (!deviceState.mHasHeadTracker) {
-                    deviceState.mHasHeadTracker = true;
-                    mAudioService.persistSpatialAudioDeviceSettings();
-                    logDeviceState(deviceState, "setHasHeadTracker");
-                }
-                return deviceState.mHeadTrackerEnabled;
+        final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        if (deviceState != null) {
+            if (!deviceState.mHasHeadTracker) {
+                deviceState.mHasHeadTracker = true;
+                mAudioService.persistSpatialAudioDeviceSettings();
+                logDeviceState(deviceState, "setHasHeadTracker");
             }
+            return deviceState.mHeadTrackerEnabled;
         }
         Log.e(TAG, "setHasHeadTracker: device not found for:" + ada);
         return false;
@@ -1150,15 +1142,9 @@
             Log.v(TAG, "no headtracking support, isHeadTrackerEnabled always false for " + ada);
             return false;
         }
-        for (SADeviceState deviceState : mSADevices) {
-            if (deviceState.matchesAudioDeviceAttributes(ada)) {
-                if (!deviceState.mHasHeadTracker) {
-                    return false;
-                }
-                return deviceState.mHeadTrackerEnabled;
-            }
-        }
-        return false;
+        final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada);
+        return deviceState != null
+                && deviceState.mHasHeadTracker && deviceState.mHeadTrackerEnabled;
     }
 
     synchronized boolean isHeadTrackerAvailable() {
@@ -1582,12 +1568,6 @@
                     mDeviceType, mDeviceAddress == null ? "" : mDeviceAddress);
         }
 
-        public boolean matchesAudioDeviceAttributes(AudioDeviceAttributes ada) {
-            final int deviceType = ada.getType();
-            final boolean wireless = isWireless(deviceType);
-            return (deviceType == mDeviceType)
-                        && (!wireless || ada.getAddress().equals(mDeviceAddress));
-        }
     }
 
     /*package*/ synchronized String getSADeviceSettings() {
@@ -1608,7 +1588,10 @@
         // small list, not worth overhead of Arrays.stream(devSettings)
         for (String setting : devSettings) {
             SADeviceState devState = SADeviceState.fromPersistedString(setting);
+            // Note if the device is not compatible with spatialization mode
+            // or the device type is not canonical, it is ignored.
             if (devState != null
+                    && devState.mDeviceType == getCanonicalDeviceType(devState.mDeviceType)
                     && isDeviceCompatibleWithSpatializationModes(
                             devState.getAudioDeviceAttributes())) {
                 mSADevices.add(devState);
@@ -1645,15 +1628,6 @@
         return false;
     }
 
-    private static boolean isWirelessSpeaker(@AudioDeviceInfo.AudioDeviceType int deviceType) {
-        for (int type : WIRELESS_SPEAKER_TYPES) {
-            if (type == deviceType) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private int getHeadSensorHandleUpdateTracker() {
         int headHandle = -1;
         UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(ROUTING_DEVICES[0]);
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java b/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java
index 1b24aa8..0d789f7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java
@@ -108,6 +108,17 @@
         }
     }
 
+    @Override
+    public void onBiometricAction(@BiometricStateListener.Action int action) {
+        for (IBiometricStateListener listener : mBiometricStateListeners) {
+            try {
+                listener.onBiometricAction(action);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception in onBiometricAction", e);
+            }
+        }
+    }
+
     /**
      * This should be invoked when:
      * 1) Enrolled --> None-enrolled
diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallback.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallback.java
index 8ea4ee9..4417f92 100644
--- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallback.java
+++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallback.java
@@ -31,6 +31,11 @@
     default void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {}
 
     /**
+     * Invoked when a biometric action has occurred.
+     */
+    default void onBiometricAction(int action) {}
+
+    /**
      * Invoked when the ClientMonitor operation is complete. This abstracts away asynchronous
      * (i.e. Authenticate, Enroll, Enumerate, Remove) and synchronous (i.e. generateChallenge,
      * revokeChallenge) so that a scheduler can process ClientMonitors regardless of their
diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCompositeCallback.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCompositeCallback.java
index b82f5fa..07041bf8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCompositeCallback.java
+++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCompositeCallback.java
@@ -44,6 +44,13 @@
     }
 
     @Override
+    public final void onBiometricAction(int action) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            mCallbacks.get(i).onBiometricAction(action);
+        }
+    }
+
+    @Override
     public final void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
             boolean success) {
         for (int i = mCallbacks.size() - 1; i >= 0; i--) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 0c5b19b..94b67ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -1020,9 +1020,18 @@
 
         @Override
         public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
+            Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL);
             FingerprintService.this.registerBiometricStateListener(listener);
         }
-    }
+
+        @Override
+        public void onPowerPressed() {
+            Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL);
+            for (ServiceProvider provider : mServiceProviders) {
+                provider.onPowerPressed();
+            }
+        }
+    };
 
     public FingerprintService(Context context) {
         super(context);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/PowerPressHandler.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/PowerPressHandler.java
new file mode 100644
index 0000000..288c372
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/PowerPressHandler.java
@@ -0,0 +1,27 @@
+/*
+ * 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.server.biometrics.sensors.fingerprint;
+
+/**
+ * Interface for handling power presses.
+ */
+public interface PowerPressHandler {
+    /**
+     * Indicates a power press has occurred.
+     */
+    void onPowerPressed();
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
index 9cdbdc9..24a47e0 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
@@ -150,6 +150,8 @@
 
     void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller);
 
+    void onPowerPressed();
+
     /**
      * Sets side-fps controller
      * @param controller side-fps controller
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 79e3bf53..e1626f0 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -21,6 +21,7 @@
 import android.app.TaskStackListener;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
 import android.hardware.biometrics.common.ICancellationSignal;
@@ -29,10 +30,15 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
+import android.os.Build;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
 import android.util.Slog;
 
+import com.android.internal.R;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.CallbackWithProbe;
@@ -46,17 +52,18 @@
 import com.android.server.biometrics.sensors.LockoutConsumer;
 import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.SensorOverlays;
+import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 
 import java.util.ArrayList;
 import java.util.function.Supplier;
 
 /**
- * Fingerprint-specific authentication client supporting the
- * {@link android.hardware.biometrics.fingerprint.IFingerprint} AIDL interface.
+ * Fingerprint-specific authentication client supporting the {@link
+ * android.hardware.biometrics.fingerprint.IFingerprint} AIDL interface.
  */
-class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> implements
-        Udfps, LockoutConsumer {
+class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession>
+        implements Udfps, LockoutConsumer, PowerPressHandler {
     private static final String TAG = "FingerprintAuthenticationClient";
 
     @NonNull private final LockoutCache mLockoutCache;
@@ -64,33 +71,88 @@
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
     @NonNull private final CallbackWithProbe<Probe> mALSProbeCallback;
 
-    @Nullable private ICancellationSignal mCancellationSignal;
+    @Nullable
+    private ICancellationSignal mCancellationSignal;
     private boolean mIsPointerDown;
+    private final Handler mHandler;
 
-    FingerprintAuthenticationClient(@NonNull Context context,
+    private static final int MESSAGE_IGNORE_AUTH = 1;
+    private static final int MESSAGE_AUTH_SUCCESS = 2;
+    private long mWaitForAuthKeyguard;
+    private long mWaitForAuthBp;
+    private long mIgnoreAuthFor;
+
+    FingerprintAuthenticationClient(
+            @NonNull Context context,
             @NonNull Supplier<AidlSession> lazyDaemon,
-            @NonNull IBinder token, long requestId,
-            @NonNull ClientMonitorCallbackConverter listener, int targetUserId, long operationId,
-            boolean restricted, @NonNull String owner, int cookie, boolean requireConfirmation,
+            @NonNull IBinder token,
+            long requestId,
+            @NonNull ClientMonitorCallbackConverter listener,
+            int targetUserId,
+            long operationId,
+            boolean restricted,
+            @NonNull String owner,
+            int cookie,
+            boolean requireConfirmation,
             int sensorId,
-            @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
+            @NonNull BiometricLogger biometricLogger,
+            @NonNull BiometricContext biometricContext,
             boolean isStrongBiometric,
-            @Nullable TaskStackListener taskStackListener, @NonNull LockoutCache lockoutCache,
+            @Nullable TaskStackListener taskStackListener,
+            @NonNull LockoutCache lockoutCache,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
             boolean allowBackgroundAuthentication,
-            @NonNull FingerprintSensorPropertiesInternal sensorProps) {
-        super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted, owner,
-                cookie, requireConfirmation, sensorId,
-                biometricLogger, biometricContext,
-                isStrongBiometric, taskStackListener,
-                lockoutCache, allowBackgroundAuthentication, true /* shouldVibrate */,
+            @NonNull FingerprintSensorPropertiesInternal sensorProps,
+            @NonNull Handler handler) {
+        super(
+                context,
+                lazyDaemon,
+                token,
+                listener,
+                targetUserId,
+                operationId,
+                restricted,
+                owner,
+                cookie,
+                requireConfirmation,
+                sensorId,
+                biometricLogger,
+                biometricContext,
+                isStrongBiometric,
+                taskStackListener,
+                lockoutCache,
+                allowBackgroundAuthentication,
+                true /* shouldVibrate */,
                 false /* isKeyguardBypassEnabled */);
         setRequestId(requestId);
         mLockoutCache = lockoutCache;
         mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().createALSCallback(false /* startWithClient */);
+        mHandler = handler;
+
+        mWaitForAuthKeyguard =
+                context.getResources()
+                        .getInteger(R.integer.config_sidefpsKeyguardPowerPressWindow);
+        mWaitForAuthBp =
+                context.getResources().getInteger(R.integer.config_sidefpsBpPowerPressWindow);
+        mIgnoreAuthFor =
+                context.getResources().getInteger(R.integer.config_sidefpsPostAuthDowntime);
+
+        if (mSensorProps.isAnySidefpsType()) {
+            if (Build.isDebuggable()) {
+                mWaitForAuthKeyguard = Settings.Secure.getIntForUser(context.getContentResolver(),
+                        Settings.Secure.FINGERPRINT_SIDE_FPS_KG_POWER_WINDOW,
+                        (int) mWaitForAuthKeyguard, UserHandle.USER_CURRENT);
+                mWaitForAuthBp = Settings.Secure.getIntForUser(context.getContentResolver(),
+                        Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW, (int) mWaitForAuthBp,
+                        UserHandle.USER_CURRENT);
+                mIgnoreAuthFor = Settings.Secure.getIntForUser(context.getContentResolver(),
+                        Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, (int) mIgnoreAuthFor,
+                        UserHandle.USER_CURRENT);
+            }
+        }
     }
 
     @Override
@@ -108,8 +170,8 @@
     @NonNull
     @Override
     protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) {
-        return new ClientMonitorCompositeCallback(mALSProbeCallback,
-                getBiometricContextUnsubscriber(), callback);
+        return new ClientMonitorCompositeCallback(
+                mALSProbeCallback, getBiometricContextUnsubscriber(), callback);
     }
 
     @Override
@@ -126,16 +188,37 @@
     }
 
     @Override
-    public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
-            boolean authenticated, ArrayList<Byte> token) {
-        super.onAuthenticated(identifier, authenticated, token);
+    public void onAuthenticated(
+            BiometricAuthenticator.Identifier identifier,
+            boolean authenticated,
+            ArrayList<Byte> token) {
 
-        if (authenticated) {
-            mState = STATE_STOPPED;
-            mSensorOverlays.hide(getSensorId());
-        } else {
-            mState = STATE_STARTED_PAUSED_ATTEMPTED;
+        long delay = 0;
+        if (authenticated && mSensorProps.isAnySidefpsType()) {
+            if (mHandler.hasMessages(MESSAGE_IGNORE_AUTH)) {
+                Slog.i(TAG, "(sideFPS) Ignoring auth due to recent power press");
+                onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true);
+                return;
+            }
+            delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
+            Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps waiting for power until: " + delay);
         }
+
+        mHandler.postDelayed(
+                () -> {
+                    if (authenticated && mSensorProps.isAnySidefpsType()) {
+                        Slog.i(TAG, "(sideFPS): No power press detected, sending auth");
+                    }
+                    super.onAuthenticated(identifier, authenticated, token);
+                    if (authenticated) {
+                        mState = STATE_STOPPED;
+                        mSensorOverlays.hide(getSensorId());
+                    } else {
+                        mState = STATE_STARTED_PAUSED_ATTEMPTED;
+                    }
+                },
+                MESSAGE_AUTH_SUCCESS,
+                delay);
     }
 
     @Override
@@ -165,7 +248,8 @@
             mCancellationSignal = doAuthenticate();
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
-            onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
+            onError(
+                    BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
             mSensorOverlays.hide(getSensorId());
             mCallback.onClientFinished(this, false /* success */);
@@ -177,15 +261,18 @@
 
         if (session.hasContextMethods()) {
             final OperationContext opContext = getOperationContext();
-            final ICancellationSignal cancel =  session.getSession().authenticateWithContext(
-                    mOperationId, opContext);
-            getBiometricContext().subscribe(opContext, ctx -> {
-                try {
-                    session.getSession().onContextChanged(ctx);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Unable to notify context changed", e);
-                }
-            });
+            final ICancellationSignal cancel =
+                    session.getSession().authenticateWithContext(mOperationId, opContext);
+            getBiometricContext()
+                    .subscribe(
+                            opContext,
+                            ctx -> {
+                                try {
+                                    session.getSession().onContextChanged(ctx);
+                                } catch (RemoteException e) {
+                                    Slog.e(TAG, "Unable to notify context changed", e);
+                                }
+                            });
             return cancel;
         } else {
             return session.getSession().authenticate(mOperationId);
@@ -202,7 +289,8 @@
                 mCancellationSignal.cancel();
             } catch (RemoteException e) {
                 Slog.e(TAG, "Remote exception", e);
-                onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
+                onError(
+                        BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                         0 /* vendorCode */);
                 mCallback.onClientFinished(this, false /* success */);
             }
@@ -284,8 +372,13 @@
         mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_TIMED);
         // Lockout metrics are logged as an error code.
         final int error = BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
-        getLogger().logOnError(getContext(), getOperationContext(),
-                error, 0 /* vendorCode */, getTargetUserId());
+        getLogger()
+                .logOnError(
+                        getContext(),
+                        getOperationContext(),
+                        error,
+                        0 /* vendorCode */,
+                        getTargetUserId());
 
         try {
             getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */);
@@ -303,8 +396,13 @@
         mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_PERMANENT);
         // Lockout metrics are logged as an error code.
         final int error = BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;
-        getLogger().logOnError(getContext(), getOperationContext(),
-                error, 0 /* vendorCode */, getTargetUserId());
+        getLogger()
+                .logOnError(
+                        getContext(),
+                        getOperationContext(),
+                        error,
+                        0 /* vendorCode */,
+                        getTargetUserId());
 
         try {
             getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */);
@@ -315,4 +413,19 @@
         mSensorOverlays.hide(getSensorId());
         mCallback.onClientFinished(this, false /* success */);
     }
+
+    @Override
+    public void onPowerPressed() {
+        if (mSensorProps.isAnySidefpsType()) {
+            Slog.i(TAG, "(sideFPS): onPowerPressed");
+            if (mHandler.hasMessages(MESSAGE_AUTH_SUCCESS)) {
+                Slog.i(TAG, "(sideFPS): Ignoring auth in queue");
+                mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
+                // Do not call onError() as that will send an additional callback to coex.
+                onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true);
+            }
+            mHandler.removeMessages(MESSAGE_IGNORE_AUTH);
+            mHandler.postDelayed(() -> {}, MESSAGE_IGNORE_AUTH, mIgnoreAuthFor);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index f23659c..f4f0a19 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -22,6 +22,7 @@
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
+import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.biometrics.common.ICancellationSignal;
 import android.hardware.biometrics.common.OperationContext;
 import android.hardware.biometrics.fingerprint.PointerContext;
@@ -139,7 +140,7 @@
                 controller.onEnrollmentHelp(getSensorId());
             }
         });
-
+        mCallback.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
         super.onAcquired(acquiredInfo, vendorCode);
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index f16af78..6f6c09b9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -67,6 +67,7 @@
 import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
+import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler;
 import com.android.server.biometrics.sensors.fingerprint.ServiceProvider;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 
@@ -388,6 +389,11 @@
                 }
 
                 @Override
+                public void onBiometricAction(int action) {
+                    mBiometricStateCallback.onBiometricAction(action);
+                }
+
+                @Override
                 public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
                         boolean success) {
                     mBiometricStateCallback.onClientFinished(clientMonitor, success);
@@ -441,7 +447,7 @@
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener, mSensors.get(sensorId).getLockoutCache(),
                     mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication,
-                    mSensors.get(sensorId).getSensorProperties());
+                    mSensors.get(sensorId).getSensorProperties(), mHandler);
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
     }
@@ -614,6 +620,21 @@
     }
 
     @Override
+    public void onPowerPressed() {
+        for (int i = 0; i < mSensors.size(); i++) {
+            final Sensor sensor = mSensors.valueAt(i);
+            BaseClientMonitor client = sensor.getScheduler().getCurrentClient();
+            if (client == null) {
+                return;
+            }
+            if (!(client instanceof PowerPressHandler)) {
+                continue;
+            }
+            ((PowerPressHandler) client).onPowerPressed();
+        }
+    }
+
+    @Override
     public void setSidefpsController(@NonNull ISidefpsController controller) {
         mSidefpsController = controller;
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index 2a3f34a..c1a8638 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -603,6 +603,11 @@
                 }
 
                 @Override
+                public void onBiometricAction(int action) {
+                    mBiometricStateCallback.onBiometricAction(action);
+                }
+
+                @Override
                 public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
                         boolean success) {
                     mBiometricStateCallback.onClientFinished(clientMonitor, success);
@@ -821,6 +826,11 @@
     }
 
     @Override
+    public void onPowerPressed() {
+        Slog.e(TAG, "onPowerPressed not supported for HIDL clients");
+    }
+
+    @Override
     public void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller) {
         mUdfpsOverlayController = controller;
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
index 1d478e5..2a59c8c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricFingerprintConstants;
+import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintManager;
@@ -151,6 +152,8 @@
                 controller.onEnrollmentHelp(getSensorId());
             }
         });
+
+        mCallback.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 076ac2b..6f9a176 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -43,6 +43,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
@@ -63,6 +64,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.SafetyProtectionUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -1181,9 +1183,19 @@
                 String message =
                         getContext().getString(R.string.pasted_from_clipboard, callingAppLabel);
                 Slog.i(TAG, message);
-                Toast.makeText(
-                        getContext(), UiThread.get().getLooper(), message, Toast.LENGTH_SHORT)
-                        .show();
+                Toast toastToShow;
+                if (SafetyProtectionUtils.shouldShowSafetyProtectionResources(getContext())) {
+                    Drawable safetyProtectionIcon = getContext()
+                            .getDrawable(R.drawable.ic_safety_protection);
+                    toastToShow = Toast.makeCustomToastWithIcon(getContext(),
+                            UiThread.get().getLooper(), message,
+                            Toast.LENGTH_SHORT, safetyProtectionIcon);
+                } else {
+                    toastToShow = Toast.makeText(
+                            getContext(), UiThread.get().getLooper(), message,
+                            Toast.LENGTH_SHORT);
+                }
+                toastToShow.show();
             } catch (PackageManager.NameNotFoundException e) {
                 // do nothing
             }
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index f526960..86b8d32 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -3281,7 +3281,7 @@
                             // All the above failures are configuration errors, and are terminal
                             // TODO(b/230548427): Remove SDK check once VPN related stuff are
                             //  decoupled from ConnectivityServiceTest.
-                            if (SdkLevel.isAtLeastT()) {
+                            if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
                                 sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_IKE_ERROR,
                                         VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
                                         ikeException.getErrorType(),
@@ -3299,7 +3299,7 @@
                             // All the above failures are configuration errors, and are terminal
                             // TODO(b/230548427): Remove SDK check once VPN related stuff are
                             //  decoupled from ConnectivityServiceTest.
-                            if (SdkLevel.isAtLeastT()) {
+                            if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
                                 sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_IKE_ERROR,
                                         VpnManager.ERROR_CLASS_RECOVERABLE,
                                         ikeException.getErrorType(),
@@ -3318,7 +3318,7 @@
                 } else if (exception instanceof IkeNetworkLostException) {
                     // TODO(b/230548427): Remove SDK check once VPN related stuff are
                     //  decoupled from ConnectivityServiceTest.
-                    if (SdkLevel.isAtLeastT()) {
+                    if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
                         sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_NETWORK_ERROR,
                                 VpnManager.ERROR_CLASS_RECOVERABLE,
                                 VpnManager.ERROR_CODE_NETWORK_LOST,
@@ -3333,7 +3333,7 @@
                     if (exception.getCause() instanceof UnknownHostException) {
                         // TODO(b/230548427): Remove SDK check once VPN related stuff are
                         //  decoupled from ConnectivityServiceTest.
-                        if (SdkLevel.isAtLeastT()) {
+                        if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
                             sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_NETWORK_ERROR,
                                     VpnManager.ERROR_CLASS_RECOVERABLE,
                                     VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST,
@@ -3347,7 +3347,7 @@
                     } else if (exception.getCause() instanceof IkeTimeoutException) {
                         // TODO(b/230548427): Remove SDK check once VPN related stuff are
                         //  decoupled from ConnectivityServiceTest.
-                        if (SdkLevel.isAtLeastT()) {
+                        if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
                             sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_NETWORK_ERROR,
                                     VpnManager.ERROR_CLASS_RECOVERABLE,
                                     VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT,
@@ -3361,7 +3361,7 @@
                     } else if (exception.getCause() instanceof IOException) {
                         // TODO(b/230548427): Remove SDK check once VPN related stuff are
                         //  decoupled from ConnectivityServiceTest.
-                        if (SdkLevel.isAtLeastT()) {
+                        if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
                             sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_NETWORK_ERROR,
                                     VpnManager.ERROR_CLASS_RECOVERABLE,
                                     VpnManager.ERROR_CODE_NETWORK_IO,
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index 9fb1d8e..625f193 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -871,10 +871,9 @@
         if (!mPendingIntentRequest.hasPendingIntent() && mRegistered) {
             mClientManager.unregisterClient(mHostEndPointId);
             mRegistered = false;
+            mAppOpsManager.stopWatchingMode(this);
+            mContextHubProxy.onHostEndpointDisconnected(mHostEndPointId);
         }
-        mAppOpsManager.stopWatchingMode(this);
-
-        mContextHubProxy.onHostEndpointDisconnected(mHostEndPointId);
     }
 
     private String authStateToString(@ContextHubManager.AuthorizationState int state) {
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
index a9b2570..7fe2f81 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
@@ -16,15 +16,8 @@
 
 package com.android.server.notification;
 
-import android.app.AlarmManager;
 import android.app.NotificationHistory;
 import android.app.NotificationHistory.HistoricalNotification;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.Uri;
 import android.os.Handler;
 import android.util.AtomicFile;
 import android.util.Slog;
@@ -60,18 +53,9 @@
     private static final String TAG = "NotiHistoryDatabase";
     private static final boolean DEBUG = NotificationManagerService.DBG;
     private static final int HISTORY_RETENTION_DAYS = 1;
-    private static final int HISTORY_RETENTION_MS = 24 * 60 * 60 * 1000;
     private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20;
     private static final long INVALID_FILE_TIME_MS = -1;
 
-    private static final String ACTION_HISTORY_DELETION =
-            NotificationHistoryDatabase.class.getSimpleName() + ".CLEANUP";
-    private static final int REQUEST_CODE_DELETION = 1;
-    private static final String SCHEME_DELETION = "delete";
-    private static final String EXTRA_KEY = "key";
-
-    private final Context mContext;
-    private final AlarmManager mAlarmManager;
     private final Object mLock = new Object();
     private final Handler mFileWriteHandler;
     @VisibleForTesting
@@ -87,9 +71,7 @@
     @VisibleForTesting
     NotificationHistory mBuffer;
 
-    public NotificationHistoryDatabase(Context context, Handler fileWriteHandler, File dir) {
-        mContext = context;
-        mAlarmManager = context.getSystemService(AlarmManager.class);
+    public NotificationHistoryDatabase(Handler fileWriteHandler, File dir) {
         mCurrentVersion = DEFAULT_CURRENT_VERSION;
         mFileWriteHandler = fileWriteHandler;
         mVersionFile = new File(dir, "version");
@@ -97,11 +79,6 @@
         mHistoryFiles = new LinkedList<>();
         mBuffer = new NotificationHistory();
         mWriteBufferRunnable = new WriteBufferRunnable();
-
-        IntentFilter deletionFilter = new IntentFilter(ACTION_HISTORY_DELETION);
-        deletionFilter.addDataScheme(SCHEME_DELETION);
-        mContext.registerReceiver(mFileCleanupReceiver, deletionFilter,
-                Context.RECEIVER_EXPORTED_UNAUDITED);
     }
 
     public void init() {
@@ -117,7 +94,7 @@
 
             checkVersionAndBuildLocked();
             indexFilesLocked();
-            prune(HISTORY_RETENTION_DAYS, System.currentTimeMillis());
+            prune();
         }
     }
 
@@ -246,7 +223,14 @@
     }
 
     /**
-     * Remove any files that are too old and schedule jobs to clean up the rest
+     * Remove any files that are too old.
+     */
+    void prune() {
+        prune(HISTORY_RETENTION_DAYS, System.currentTimeMillis());
+    }
+
+    /**
+     * Remove any files that are too old.
      */
     void prune(final int retentionDays, final long currentTimeMillis) {
         synchronized (mLock) {
@@ -265,10 +249,6 @@
 
                 if (creationTime <= retentionBoundary.getTimeInMillis()) {
                     deleteFile(currentOldestFile);
-                } else {
-                    // all remaining files are newer than the cut off; schedule jobs to delete
-                    scheduleDeletion(
-                            currentOldestFile.getBaseFile(), creationTime, retentionDays);
                 }
             }
         }
@@ -306,26 +286,6 @@
         removeFilePathFromHistory(file.getBaseFile().getAbsolutePath());
     }
 
-    private void scheduleDeletion(File file, long creationTime, int retentionDays) {
-        final long deletionTime = creationTime + (retentionDays * HISTORY_RETENTION_MS);
-        scheduleDeletion(file, deletionTime);
-    }
-
-    private void scheduleDeletion(File file, long deletionTime) {
-        if (DEBUG) {
-            Slog.d(TAG, "Scheduling deletion for " + file.getName() + " at " + deletionTime);
-        }
-        final PendingIntent pi = PendingIntent.getBroadcast(mContext,
-                REQUEST_CODE_DELETION,
-                new Intent(ACTION_HISTORY_DELETION)
-                        .setData(new Uri.Builder().scheme(SCHEME_DELETION)
-                                .appendPath(file.getAbsolutePath()).build())
-                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
-                        .putExtra(EXTRA_KEY, file.getAbsolutePath()),
-                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-        mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, deletionTime, pi);
-    }
-
     private void writeLocked(AtomicFile file, NotificationHistory notifications)
             throws IOException {
         FileOutputStream fos = file.startWrite();
@@ -355,12 +315,6 @@
         }
     }
 
-    public void unregisterFileCleanupReceiver() {
-        if(mContext != null) {
-            mContext.unregisterReceiver(mFileCleanupReceiver);
-        }
-    }
-
     private static long safeParseLong(String fileName) {
         // AtomicFile will create copies of the numeric files with ".new" and ".bak"
         // over the course of its processing. If these files still exist on boot we need to clean
@@ -372,40 +326,15 @@
         }
     }
 
-    private final BroadcastReceiver mFileCleanupReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (action == null) {
-                return;
-            }
-            if (ACTION_HISTORY_DELETION.equals(action)) {
-                try {
-                    synchronized (mLock) {
-                        final String filePath = intent.getStringExtra(EXTRA_KEY);
-                        AtomicFile fileToDelete = new AtomicFile(new File(filePath));
-                        if (DEBUG) {
-                            Slog.d(TAG, "Removed " + fileToDelete.getBaseFile().getName());
-                        }
-                        fileToDelete.delete();
-                        removeFilePathFromHistory(filePath);
-                    }
-                } catch (Exception e) {
-                    Slog.e(TAG, "Failed to delete notification history file", e);
-                }
-            }
-        }
-    };
-
     final class WriteBufferRunnable implements Runnable {
 
         @Override
         public void run() {
             long time = System.currentTimeMillis();
-            run(time, new AtomicFile(new File(mHistoryDir, String.valueOf(time))));
+            run(new AtomicFile(new File(mHistoryDir, String.valueOf(time))));
         }
 
-        void run(long time, AtomicFile file) {
+        void run(AtomicFile file) {
             synchronized (mLock) {
                 if (DEBUG) Slog.d(TAG, "WriteBufferRunnable "
                         + file.getBaseFile().getAbsolutePath());
@@ -413,8 +342,6 @@
                     writeLocked(file, mBuffer);
                     mHistoryFiles.addFirst(file);
                     mBuffer = new NotificationHistory();
-
-                    scheduleDeletion(file.getBaseFile(), time, HISTORY_RETENTION_DAYS);
                 } catch (IOException e) {
                     Slog.e(TAG, "Failed to write buffer to disk. not flushing buffer", e);
                 }
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java
index d9e0d79..0d975cc 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java
@@ -35,6 +35,6 @@
         if(sTestingNotificationHistoryDb != null) {
             return sTestingNotificationHistoryDb;
         }
-        return new NotificationHistoryDatabase(context, handler, rootDir);
+        return new NotificationHistoryDatabase(handler, rootDir);
     }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryJobService.java b/services/core/java/com/android/server/notification/NotificationHistoryJobService.java
new file mode 100644
index 0000000..3776ad7
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryJobService.java
@@ -0,0 +1,81 @@
+/*
+ * 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.server.notification;
+
+import static android.app.job.JobScheduler.RESULT_SUCCESS;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This service runs every twenty minutes to ensure the retention policy for notification history
+ * data.
+ */
+public class NotificationHistoryJobService extends JobService {
+    private final static String TAG = "NotificationHistoryJob";
+    private static final long JOB_RUN_INTERVAL = TimeUnit.MINUTES.toMillis(20);
+
+    static final int BASE_JOB_ID = 237039804;
+
+    static void scheduleJob(Context context) {
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        if (jobScheduler.getPendingJob(BASE_JOB_ID) == null) {
+            ComponentName component =
+                    new ComponentName(context, NotificationHistoryJobService.class);
+            JobInfo newJob = new JobInfo.Builder(BASE_JOB_ID, component)
+                    .setRequiresDeviceIdle(false)
+                    .setPeriodic(JOB_RUN_INTERVAL)
+                    .build();
+            if (jobScheduler.schedule(newJob) != RESULT_SUCCESS) {
+                Slog.w(TAG, "Failed to schedule history cleanup job");
+            }
+        }
+    }
+
+    private CancellationSignal mSignal;
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        mSignal = new CancellationSignal();
+        new Thread(() -> {
+            NotificationManagerInternal nmInternal =
+                    LocalServices.getService(NotificationManagerInternal.class);
+            nmInternal.cleanupHistoryFiles();
+            jobFinished(params, mSignal.isCanceled());
+        }).start();
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        if (mSignal != null) {
+            mSignal.cancel();
+        }
+        return false;
+    }
+}
+
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
index 0aacd13..6a46048 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryManager.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
@@ -84,6 +84,11 @@
     }
 
     void onBootPhaseAppsCanStart() {
+        try {
+            NotificationHistoryJobService.scheduleJob(mContext);
+        } catch (Throwable e) {
+            Slog.e(TAG, "Failed to schedule cleanup job", e);
+        }
         mSettingsObserver.observe();
     }
 
@@ -151,6 +156,24 @@
         }
     }
 
+    public void cleanupHistoryFiles() {
+        synchronized (mLock) {
+            int n = mUserUnlockedStates.size();
+            for (int i = 0;  i < n; i++) {
+                // cleanup old files for currently unlocked users. User are additionally cleaned
+                // on unlock in NotificationHistoryDatabase.init().
+                if (mUserUnlockedStates.valueAt(i)) {
+                    final NotificationHistoryDatabase userHistory =
+                            mUserState.get(mUserUnlockedStates.keyAt(i));
+                    if (userHistory == null) {
+                        continue;
+                    }
+                    userHistory.prune();
+                }
+            }
+        }
+    }
+
     public void deleteNotificationHistoryItem(String pkg, int uid, long postedTime) {
         synchronized (mLock) {
             int userId = UserHandle.getUserId(uid);
@@ -288,7 +311,6 @@
 
     private void disableHistory(NotificationHistoryDatabase userHistory, @UserIdInt int userId) {
         userHistory.disableHistory();
-        userHistory.unregisterFileCleanupReceiver();
 
         mUserPendingHistoryDisables.put(userId, false);
         mHistoryEnabled.put(userId, false);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerInternal.java b/services/core/java/com/android/server/notification/NotificationManagerInternal.java
index 8a62736..bc38856 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerInternal.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerInternal.java
@@ -45,4 +45,6 @@
 
     /** Send a notification to the user prompting them to review their notification permissions. */
     void sendReviewPermissionsNotification();
+
+    void cleanupHistoryFiles();
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e589080..793f592d 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2733,7 +2733,7 @@
     }
 
     @Override
-    public void onUserUnlocking(@NonNull TargetUser user) {
+    public void onUserUnlocked(@NonNull TargetUser user) {
         mHandler.post(() -> {
             Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "notifHistoryUnlockUser");
             try {
@@ -6345,6 +6345,12 @@
                     Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
                     NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
         }
+
+        @Override
+        public void cleanupHistoryFiles() {
+            checkCallerIsSystem();
+            mHistoryManager.cleanupHistoryFiles();
+        }
     };
 
     int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted) {
@@ -6758,9 +6764,8 @@
 
     protected void doChannelWarningToast(int forUid, CharSequence toastText) {
         Binder.withCleanCallingIdentity(() -> {
-            final int defaultWarningEnabled = Build.IS_DEBUGGABLE ? 1 : 0;
             final boolean warningEnabled = Settings.Global.getInt(getContext().getContentResolver(),
-                    Settings.Global.SHOW_NOTIFICATION_CHANNEL_WARNINGS, defaultWarningEnabled) != 0;
+                    Settings.Global.SHOW_NOTIFICATION_CHANNEL_WARNINGS, 0) != 0;
             if (warningEnabled) {
                 Toast toast = Toast.makeText(getContext(), mHandler.getLooper(), toastText,
                         Toast.LENGTH_SHORT);
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 066692d..477b8da 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -86,7 +86,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -1328,17 +1327,16 @@
                 return null;
             }
             NotificationChannelGroup group = r.groups.get(groupId).clone();
-            ArrayList channels = new ArrayList();
+            group.setChannels(new ArrayList<>());
             int N = r.channels.size();
             for (int i = 0; i < N; i++) {
                 final NotificationChannel nc = r.channels.valueAt(i);
                 if (includeDeleted || !nc.isDeleted()) {
                     if (groupId.equals(nc.getGroup())) {
-                        channels.add(nc);
+                        group.addChannel(nc);
                     }
                 }
             }
-            group.setChannels(channels);
             return group;
         }
     }
@@ -1359,48 +1357,44 @@
     public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
             int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty) {
         Objects.requireNonNull(pkg);
-        List<NotificationChannelGroup> groups = new ArrayList<>();
+        Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
         synchronized (mPackagePreferences) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return ParceledListSlice.emptyList();
             }
-            Map<String, ArrayList<NotificationChannel>> groupedChannels = new HashMap();
+            NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
             int N = r.channels.size();
             for (int i = 0; i < N; i++) {
                 final NotificationChannel nc = r.channels.valueAt(i);
                 if (includeDeleted || !nc.isDeleted()) {
                     if (nc.getGroup() != null) {
                         if (r.groups.get(nc.getGroup()) != null) {
-                            ArrayList<NotificationChannel> channels = groupedChannels.getOrDefault(
-                                    nc.getGroup(), new ArrayList<>());
-                            channels.add(nc);
-                            groupedChannels.put(nc.getGroup(), channels);
+                            NotificationChannelGroup ncg = groups.get(nc.getGroup());
+                            if (ncg == null) {
+                                ncg = r.groups.get(nc.getGroup()).clone();
+                                ncg.setChannels(new ArrayList<>());
+                                groups.put(nc.getGroup(), ncg);
+
+                            }
+                            ncg.addChannel(nc);
                         }
                     } else {
-                        ArrayList<NotificationChannel> channels = groupedChannels.getOrDefault(
-                            null, new ArrayList<>());
-                        channels.add(nc);
-                        groupedChannels.put(null, channels);
+                        nonGrouped.addChannel(nc);
                     }
                 }
             }
-            for (NotificationChannelGroup group : r.groups.values()) {
-                ArrayList<NotificationChannel> channels =
-                        groupedChannels.getOrDefault(group.getId(), new ArrayList<>());
-                if (includeEmpty || !channels.isEmpty()) {
-                    NotificationChannelGroup clone = group.clone();
-                    clone.setChannels(channels);
-                    groups.add(clone);
+            if (includeNonGrouped && nonGrouped.getChannels().size() > 0) {
+                groups.put(null, nonGrouped);
+            }
+            if (includeEmpty) {
+                for (NotificationChannelGroup group : r.groups.values()) {
+                    if (!groups.containsKey(group.getId())) {
+                        groups.put(group.getId(), group);
+                    }
                 }
             }
-
-            if (includeNonGrouped && groupedChannels.containsKey(null)) {
-                NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
-                nonGrouped.setChannels(groupedChannels.get(null));
-                groups.add(nonGrouped);
-            }
-            return new ParceledListSlice<>(groups);
+            return new ParceledListSlice<>(new ArrayList<>(groups.values()));
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java
index f6b22bc..a03a16a 100644
--- a/services/core/java/com/android/server/pm/InitAppsHelper.java
+++ b/services/core/java/com/android/server/pm/InitAppsHelper.java
@@ -113,7 +113,7 @@
             mScanFlags = scanFlags;
         }
         mSystemParseFlags = mPm.getDefParseFlags() | ParsingPackageUtils.PARSE_IS_SYSTEM_DIR;
-        mSystemScanFlags = scanFlags | SCAN_AS_SYSTEM;
+        mSystemScanFlags = mScanFlags | SCAN_AS_SYSTEM;
         mExecutorService = ParallelPackageParser.makeExecutorService();
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 94e8ec5..85b0149 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1836,8 +1836,6 @@
         mAppDataHelper = new AppDataHelper(this);
         mInstallPackageHelper = new InstallPackageHelper(this, mAppDataHelper);
         mRemovePackageHelper = new RemovePackageHelper(this, mAppDataHelper);
-        mInitAppsHelper = new InitAppsHelper(this, mApexManager, mInstallPackageHelper,
-                mInjector.getSystemPartitions());
         mDeletePackageHelper = new DeletePackageHelper(this, mRemovePackageHelper,
                 mAppDataHelper);
         mSharedLibraries.setDeletePackageHelper(mDeletePackageHelper);
@@ -1958,6 +1956,9 @@
                         + ver.fingerprint + " to " + PackagePartitions.FINGERPRINT);
             }
 
+            mInitAppsHelper = new InitAppsHelper(this, mApexManager, mInstallPackageHelper,
+                mInjector.getSystemPartitions());
+
             // when upgrading from pre-M, promote system app permissions from install to runtime
             mPromoteSystemApps =
                     mIsUpgrade && ver.sdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1;
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index d88949b..d645bb2 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -949,6 +949,11 @@
     }
 
     private void powerPress(long eventTime, int count, boolean beganFromNonInteractive) {
+        // SideFPS still needs to know about suppressed power buttons, in case it needs to block
+        // an auth attempt.
+        if (count == 1) {
+            mSideFpsEventHandler.notifyPowerPressed();
+        }
         if (mDefaultDisplayPolicy.isScreenOnEarly() && !mDefaultDisplayPolicy.isScreenOnFully()) {
             Slog.i(TAG, "Suppressed redundant power key press while "
                     + "already in the process of turning the screen on.");
@@ -968,7 +973,7 @@
         } else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
             Slog.d(TAG, "No behavior defined for power press count " + count);
         } else if (count == 1 && interactive && !beganFromNonInteractive) {
-            if (mSideFpsEventHandler.onSinglePressDetected(eventTime)) {
+            if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) {
                 Slog.i(TAG, "Suppressing power key because the user is interacting with the "
                         + "fingerprint sensor");
                 return;
diff --git a/services/core/java/com/android/server/policy/SideFpsEventHandler.java b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
index 41d0272..af2b902 100644
--- a/services/core/java/com/android/server/policy/SideFpsEventHandler.java
+++ b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
@@ -19,6 +19,7 @@
 import static android.hardware.biometrics.BiometricStateListener.STATE_BP_AUTH;
 import static android.hardware.biometrics.BiometricStateListener.STATE_ENROLLING;
 import static android.hardware.biometrics.BiometricStateListener.STATE_IDLE;
+import static android.hardware.biometrics.BiometricStateListener.STATE_KEYGUARD_AUTH;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,8 +35,12 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
+import android.os.Build;
 import android.os.Handler;
 import android.os.PowerManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
 import android.view.WindowManager;
 
 import com.android.internal.R;
@@ -46,14 +51,26 @@
 import java.util.function.Supplier;
 
 /**
- * Defines behavior for handling interactions between power button events and
- * fingerprint-related operations, for devices where the fingerprint sensor (side fps)
- * lives on the power button.
+ * Defines behavior for handling interactions between power button events and fingerprint-related
+ * operations, for devices where the fingerprint sensor (side fps) lives on the power button.
  */
 public class SideFpsEventHandler {
 
     private static final int DEBOUNCE_DELAY_MILLIS = 500;
 
+    private int getTapWaitForPowerDuration(Context context) {
+        int tap = context.getResources().getInteger(
+                R.integer.config_sidefpsEnrollPowerPressWindow);
+        if (Build.isDebuggable()) {
+            tap = Settings.Secure.getIntForUser(context.getContentResolver(),
+                    Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, tap,
+                    UserHandle.USER_CURRENT);
+        }
+        return tap;
+    }
+
+    private static final String TAG = "SideFpsEventHandler";
+
     @NonNull private final Context mContext;
     @NonNull private final Handler mHandler;
     @NonNull private final PowerManager mPowerManager;
@@ -61,20 +78,26 @@
     @NonNull private final AtomicBoolean mSideFpsEventHandlerReady;
 
     @Nullable private Dialog mDialog;
-    @NonNull private final DialogInterface.OnDismissListener mDialogDismissListener = (dialog) -> {
-        if (mDialog == dialog) {
-            mDialog = null;
-        }
-    };
+    private final Runnable mTurnOffDialog =
+            () -> {
+                dismissDialog("mTurnOffDialog");
+            };
+
+    @NonNull private final DialogInterface.OnDismissListener mDialogDismissListener;
 
     private @BiometricStateListener.State int mBiometricState;
+    private final int mTapWaitForPowerDuration;
+    private FingerprintManager mFingerprintManager;
 
     SideFpsEventHandler(Context context, Handler handler, PowerManager powerManager) {
         this(context, handler, powerManager, () -> new AlertDialog.Builder(context));
     }
 
     @VisibleForTesting
-    SideFpsEventHandler(Context context, Handler handler, PowerManager powerManager,
+    SideFpsEventHandler(
+            Context context,
+            Handler handler,
+            PowerManager powerManager,
             Supplier<AlertDialog.Builder> dialogSupplier) {
         mContext = context;
         mHandler = handler;
@@ -82,91 +105,131 @@
         mDialogSupplier = dialogSupplier;
         mBiometricState = STATE_IDLE;
         mSideFpsEventHandlerReady = new AtomicBoolean(false);
+        mDialogDismissListener =
+                (dialog) -> {
+                    if (mDialog == dialog) {
+                        if (mHandler != null) {
+                            mHandler.removeCallbacks(mTurnOffDialog);
+                        }
+                        mDialog = null;
+                    }
+                };
 
         // ensure dialog is dismissed if screen goes off for unrelated reasons
-        context.registerReceiver(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (mDialog != null) {
-                    mDialog.dismiss();
-                    mDialog = null;
-                }
-            }
-        }, new IntentFilter(Intent.ACTION_SCREEN_OFF));
+        context.registerReceiver(
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        if (mDialog != null) {
+                            mDialog.dismiss();
+                            mDialog = null;
+                        }
+                    }
+                },
+                new IntentFilter(Intent.ACTION_SCREEN_OFF));
+        mTapWaitForPowerDuration = getTapWaitForPowerDuration(context);
     }
 
     /**
-     * Called from {@link PhoneWindowManager} after the power button is pressed and displays a
-     * dialog confirming the user's intent to turn screen off if a fingerprint operation is
-     * active. The device goes to sleep if confirmed otherwise the dialog is dismissed.
+     * Called from {@link PhoneWindowManager} to notify FingerprintManager that a single tap power
+     * button has been pressed.
+     */
+    public void notifyPowerPressed() {
+        Log.i(TAG, "notifyPowerPressed");
+        if (mFingerprintManager == null) {
+            mFingerprintManager = mContext.getSystemService(FingerprintManager.class);
+        }
+        if (mFingerprintManager == null) {
+            return;
+        }
+        mFingerprintManager.onPowerPressed();
+    }
+
+    /**
+     * Called from {@link PhoneWindowManager} and will dictate if the SideFpsEventHandler should
+     * handle the power press.
      *
      * @param eventTime powerPress event time
      * @return true if powerPress was consumed, false otherwise
      */
-    public boolean onSinglePressDetected(long eventTime) {
+    public boolean shouldConsumeSinglePress(long eventTime) {
         if (!mSideFpsEventHandlerReady.get()) {
             return false;
         }
 
         switch (mBiometricState) {
             case STATE_ENROLLING:
-            case STATE_BP_AUTH:
-                mHandler.post(() -> {
-                    if (mDialog != null) {
-                        mDialog.dismiss();
-                    }
-                    mDialog = showConfirmDialog(mDialogSupplier.get(),
-                            mPowerManager, eventTime, mBiometricState, mDialogDismissListener);
-                });
+                mHandler.post(
+                        () -> {
+                            if (mHandler.hasCallbacks(mTurnOffDialog)) {
+                                Log.v(TAG, "Detected a tap to turn off dialog, ignoring");
+                                mHandler.removeCallbacks(mTurnOffDialog);
+                            }
+                        });
+                showDialog(eventTime, "Enroll Power Press");
                 return true;
+            case STATE_BP_AUTH:
+                return true;
+            case STATE_KEYGUARD_AUTH:
             default:
                 return false;
         }
     }
 
     @NonNull
-    private static Dialog showConfirmDialog(@NonNull AlertDialog.Builder dialogBuilder,
-            @NonNull PowerManager powerManager, long eventTime,
+    private static Dialog showConfirmDialog(
+            @NonNull AlertDialog.Builder dialogBuilder,
+            @NonNull PowerManager powerManager,
+            long eventTime,
             @BiometricStateListener.State int biometricState,
             @NonNull DialogInterface.OnDismissListener dismissListener) {
         final boolean enrolling = biometricState == STATE_ENROLLING;
-        final int title = enrolling ? R.string.fp_power_button_enrollment_title
-                : R.string.fp_power_button_bp_title;
-        final int message = enrolling ? R.string.fp_power_button_enrollment_message
-                : R.string.fp_power_button_bp_message;
-        final int positiveText = enrolling ? R.string.fp_power_button_enrollment_positive_button
-                : R.string.fp_power_button_bp_positive_button;
-        final int negativeText = enrolling ? R.string.fp_power_button_enrollment_negative_button
-                : R.string.fp_power_button_bp_negative_button;
+        final int title =
+                enrolling
+                        ? R.string.fp_power_button_enrollment_title
+                        : R.string.fp_power_button_bp_title;
+        final int message =
+                enrolling
+                        ? R.string.fp_power_button_enrollment_message
+                        : R.string.fp_power_button_bp_message;
+        final int positiveText =
+                enrolling
+                        ? R.string.fp_power_button_enrollment_positive_button
+                        : R.string.fp_power_button_bp_positive_button;
+        final int negativeText =
+                enrolling
+                        ? R.string.fp_power_button_enrollment_negative_button
+                        : R.string.fp_power_button_bp_negative_button;
 
-        final Dialog confirmScreenOffDialog = dialogBuilder
-                .setTitle(title)
-                .setMessage(message)
-                .setPositiveButton(positiveText,
-                        (dialog, which) -> {
-                            dialog.dismiss();
-                            powerManager.goToSleep(
-                                    eventTime,
-                                    PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON,
-                                    0 /* flags */
-                            );
-                        })
-                .setNegativeButton(negativeText, (dialog, which) -> dialog.dismiss())
-                .setOnDismissListener(dismissListener)
-                .setCancelable(false)
-                .create();
-        confirmScreenOffDialog.getWindow().setType(
-                WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+        final Dialog confirmScreenOffDialog =
+                dialogBuilder
+                        .setTitle(title)
+                        .setMessage(message)
+                        .setPositiveButton(
+                                positiveText,
+                                (dialog, which) -> {
+                                    dialog.dismiss();
+                                    powerManager.goToSleep(
+                                            eventTime,
+                                            PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON,
+                                            0 /* flags */);
+                                })
+                        .setNegativeButton(negativeText, (dialog, which) -> dialog.dismiss())
+                        .setOnDismissListener(dismissListener)
+                        .setCancelable(false)
+                        .create();
+        confirmScreenOffDialog
+                .getWindow()
+                .setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
         confirmScreenOffDialog.show();
 
         return confirmScreenOffDialog;
     }
 
     /**
-     * Awaits notification from PhoneWindowManager that fingerprint service is ready
-     * to send updates about power button fps sensor state. Then configures a
-     * BiometricStateListener to receive and record updates to fps state, and
-     * registers the BiometricStateListener in FingerprintManager.
+     * Awaits notification from PhoneWindowManager that fingerprint service is ready to send updates
+     * about power button fps sensor state. Then configures a BiometricStateListener to receive and
+     * record updates to fps state, and registers the BiometricStateListener in FingerprintManager.
      */
     public void onFingerprintSensorReady() {
         final PackageManager pm = mContext.getPackageManager();
@@ -184,12 +247,12 @@
                         if (fingerprintManager.isPowerbuttonFps()) {
                             fingerprintManager.registerBiometricStateListener(
                                     new BiometricStateListener() {
-                                        @Nullable
-                                        private Runnable mStateRunnable = null;
+                                        @Nullable private Runnable mStateRunnable = null;
 
                                         @Override
                                         public void onStateChanged(
                                                 @BiometricStateListener.State int newState) {
+                                            Log.d(TAG, "onStateChanged : " + newState);
                                             if (mStateRunnable != null) {
                                                 mHandler.removeCallbacks(mStateRunnable);
                                                 mStateRunnable = null;
@@ -200,16 +263,58 @@
                                             // damper when moving to idle in case auth is first
                                             if (newState == STATE_IDLE) {
                                                 mStateRunnable = () -> mBiometricState = newState;
-                                                mHandler.postDelayed(mStateRunnable,
-                                                        DEBOUNCE_DELAY_MILLIS);
+                                                // This is also useful in the case of biometric
+                                                // prompt.
+                                                // If a user has recently succeeded/failed auth, we
+                                                // want to disable the power button for a short
+                                                // period of time (so ethey are able to view the
+                                                // prompt)
+                                                mHandler.postDelayed(
+                                                        mStateRunnable, DEBOUNCE_DELAY_MILLIS);
+                                                dismissDialog("STATE_IDLE");
                                             } else {
                                                 mBiometricState = newState;
                                             }
                                         }
+
+                                        @Override
+                                        public void onBiometricAction(
+                                                @BiometricStateListener.Action int action) {
+                                            Log.d(TAG, "onBiometricAction " + action);
+                                            switch (action) {
+                                                case BiometricStateListener.ACTION_SENSOR_TOUCH:
+                                                    mHandler.postDelayed(
+                                                            mTurnOffDialog,
+                                                            mTapWaitForPowerDuration);
+                                                    break;
+                                            }
+                                        }
                                     });
                             mSideFpsEventHandlerReady.set(true);
                         }
                     }
                 });
     }
+
+    private void dismissDialog(String reason) {
+        Log.d(TAG, "Dismissing dialog with reason: " + reason);
+        if (mDialog != null && mDialog.isShowing()) {
+            mDialog.dismiss();
+        }
+    }
+
+    private void showDialog(long time, String reason) {
+        Log.d(TAG, "Showing dialog with reason: " + reason);
+        if (mDialog != null && mDialog.isShowing()) {
+            Log.d(TAG, "Ignoring show dialog");
+            return;
+        }
+        mDialog =
+                showConfirmDialog(
+                        mDialogSupplier.get(),
+                        mPowerManager,
+                        time,
+                        mBiometricState,
+                        mDialogDismissListener);
+    }
 }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 50d1bd6..dbf05f1 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -4801,6 +4801,11 @@
                             .IS_STAY_ON_WHILE_PLUGGED_IN_WIRELESS,
                     ((mStayOnWhilePluggedInSetting & BatteryManager.BATTERY_PLUGGED_WIRELESS)
                             != 0));
+            proto.write(
+                    PowerServiceSettingsAndConfigurationDumpProto.StayOnWhilePluggedInProto
+                            .IS_STAY_ON_WHILE_PLUGGED_IN_DOCK,
+                    ((mStayOnWhilePluggedInSetting & BatteryManager.BATTERY_PLUGGED_DOCK)
+                            != 0));
             proto.end(stayOnWhilePluggedInToken);
 
             proto.write(
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 71b1bc2..bec3754 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -1207,7 +1207,7 @@
         private int mImeBackDisposition = 0;
         private boolean mShowImeSwitcher = false;
         private IBinder mImeToken = null;
-        private LetterboxDetails[] mLetterboxDetails;
+        private LetterboxDetails[] mLetterboxDetails = new LetterboxDetails[0];
 
         private void setBarAttributes(@Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 393c1be..550d21a 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2974,6 +2974,10 @@
                 newParent = candidateTf;
             }
         }
+        if (newParent.canHaveEmbeddingActivityTransition(mStartActivity)) {
+            // Make sure the embedded TaskFragment is included in the start activity transition.
+            newParent.collectEmbeddedTaskFragmentIfNeeded();
+        }
         if (mStartActivity.getTaskFragment() == null
                 || mStartActivity.getTaskFragment() == newParent) {
             newParent.addChild(mStartActivity, POSITION_TOP);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 90b77f7..1489891 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -3259,10 +3259,11 @@
     }
 
     public void setRotationAnimation(ScreenRotationAnimation screenRotationAnimation) {
-        if (mScreenRotationAnimation != null) {
-            mScreenRotationAnimation.kill();
-        }
+        final ScreenRotationAnimation prev = mScreenRotationAnimation;
         mScreenRotationAnimation = screenRotationAnimation;
+        if (prev != null) {
+            prev.kill();
+        }
 
         // Hide the windows which are not significant in rotation animation. So that the windows
         // don't need to block the unfreeze time.
@@ -4251,6 +4252,17 @@
         void detach(Transaction t) {
             removeImeSurface(t);
         }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(64);
+            sb.append("ImeScreenshot{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" imeTarget=" + mImeTarget);
+            sb.append(" surface=" + mImeSurface);
+            sb.append('}');
+            return sb.toString();
+        }
     }
 
     private void attachAndShowImeScreenshotOnTarget() {
@@ -4283,15 +4295,23 @@
     }
 
     /**
-     * Removes the IME screenshot when necessary.
-     *
-     * Used when app transition animation finished or obsoleted screenshot surface like size
-     * changed by rotation.
+     * Removes the IME screenshot when the caller is a part of the attached target window.
      */
-    void removeImeScreenshotIfPossible() {
-        if (mImeLayeringTarget == null
-                || mImeLayeringTarget.mAttrs.type != TYPE_APPLICATION_STARTING
-                && !mImeLayeringTarget.inTransitionSelfOrParent()) {
+    void removeImeSurfaceByTarget(WindowContainer win) {
+        if (mImeScreenshot == null || win == null) {
+            return;
+        }
+        // The starting window shouldn't be the input target to attach the IME screenshot during
+        // transitioning.
+        if (win.asWindowState() != null
+                && win.asWindowState().mAttrs.type == TYPE_APPLICATION_STARTING) {
+            return;
+        }
+
+        final WindowState screenshotTarget = mImeScreenshot.getImeTarget();
+        final boolean winIsOrContainsScreenshotTarget = (win == screenshotTarget
+                || win.getWindow(w -> w == screenshotTarget) != null);
+        if (winIsOrContainsScreenshotTarget) {
             removeImeSurfaceImmediately();
         }
     }
@@ -4639,10 +4659,8 @@
                     wc, SurfaceAnimator.animationTypeToString(type), mImeScreenshot,
                     mImeScreenshot.getImeTarget());
         }
-        if (mImeScreenshot != null && (wc == mImeScreenshot.getImeTarget()
-                || wc.getWindow(w -> w == mImeScreenshot.getImeTarget()) != null)
-                && (type & WindowState.EXIT_ANIMATING_TYPES) != 0) {
-            removeImeSurfaceImmediately();
+        if ((type & WindowState.EXIT_ANIMATING_TYPES) != 0) {
+            removeImeSurfaceByTarget(wc);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 8cad165..fd8b614 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -40,9 +40,11 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
+import android.os.IBinder;
 import android.os.Trace;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
+import android.view.DisplayAddress;
 import android.view.DisplayInfo;
 import android.view.Surface;
 import android.view.Surface.OutOfResourcesException;
@@ -165,18 +167,43 @@
         final SurfaceControl.Transaction t = mService.mTransactionFactory.get();
 
         try {
-            SurfaceControl.LayerCaptureArgs.Builder builder =
-                    new SurfaceControl.LayerCaptureArgs.Builder(displayContent.getSurfaceControl())
-                            .setCaptureSecureLayers(true)
-                            .setAllowProtected(true)
-                            .setSourceCrop(new Rect(0, 0, width, height));
-
+            final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer;
             if (isSizeChanged) {
+                final DisplayAddress address = displayInfo.address;
+                if (!(address instanceof DisplayAddress.Physical)) {
+                    Slog.e(TAG, "Display does not have a physical address: " + displayId);
+                    return;
+                }
+                final DisplayAddress.Physical physicalAddress =
+                        (DisplayAddress.Physical) address;
+                final IBinder displayToken = SurfaceControl.getPhysicalDisplayToken(
+                        physicalAddress.getPhysicalDisplayId());
+                if (displayToken == null) {
+                    Slog.e(TAG, "Display token is null.");
+                    return;
+                }
+                // Temporarily not skip screenshot for the rounded corner overlays and screenshot
+                // the whole display to include the rounded corner overlays.
+                setSkipScreenshotForRoundedCornerOverlays(false, t);
                 mRoundedCornerOverlay = displayContent.findRoundedCornerOverlays();
+                final SurfaceControl.DisplayCaptureArgs captureArgs =
+                        new SurfaceControl.DisplayCaptureArgs.Builder(displayToken)
+                                .setSourceCrop(new Rect(0, 0, width, height))
+                                .setAllowProtected(true)
+                                .setCaptureSecureLayers(true)
+                                .build();
+                screenshotBuffer = SurfaceControl.captureDisplay(captureArgs);
+            } else {
+                SurfaceControl.LayerCaptureArgs captureArgs =
+                        new SurfaceControl.LayerCaptureArgs.Builder(
+                                displayContent.getSurfaceControl())
+                                .setCaptureSecureLayers(true)
+                                .setAllowProtected(true)
+                                .setSourceCrop(new Rect(0, 0, width, height))
+                                .build();
+                screenshotBuffer = SurfaceControl.captureLayers(captureArgs);
             }
 
-            SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
-                    SurfaceControl.captureLayers(builder.build());
             if (screenshotBuffer == null) {
                 Slog.w(TAG, "Unable to take screenshot of display " + displayId);
                 return;
@@ -275,6 +302,21 @@
         t.apply();
     }
 
+    void setSkipScreenshotForRoundedCornerOverlays(
+            boolean skipScreenshot, SurfaceControl.Transaction t) {
+        mDisplayContent.forAllWindows(w -> {
+            if (!w.mToken.mRoundedCornerOverlay || !w.isVisible() || !w.mWinAnimator.hasSurface()) {
+                return;
+            }
+            t.setSkipScreenshot(w.mWinAnimator.mSurfaceController.mSurfaceControl, skipScreenshot);
+        }, false);
+        if (!skipScreenshot) {
+            // Use sync apply to apply the change immediately, so that the next
+            // SC.captureDisplay can capture the screen decor layers.
+            t.apply(true /* sync */);
+        }
+    }
+
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
         proto.write(STARTED, mStarted);
@@ -483,10 +525,15 @@
                 }
                 mBackColorSurface = null;
             }
+
             if (mRoundedCornerOverlay != null) {
-                for (SurfaceControl sc : mRoundedCornerOverlay) {
-                    if (sc.isValid()) {
-                        t.show(sc);
+                if (mDisplayContent.getRotationAnimation() == null
+                        || mDisplayContent.getRotationAnimation() == this) {
+                    setSkipScreenshotForRoundedCornerOverlays(true, t);
+                    for (SurfaceControl sc : mRoundedCornerOverlay) {
+                        if (sc.isValid()) {
+                            t.show(sc);
+                        }
                     }
                 }
                 mRoundedCornerOverlay = null;
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index f8a9d46..6888424 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2316,6 +2316,26 @@
         return !startBounds.equals(getBounds());
     }
 
+    boolean canHaveEmbeddingActivityTransition(@NonNull ActivityRecord child) {
+        if (!isOrganizedTaskFragment() || !mTransitionController.isShellTransitionsEnabled()) {
+            return false;
+        }
+        // The activity should request open transition when it is becoming visible.
+        return child.isVisibleRequested();
+    }
+
+    void collectEmbeddedTaskFragmentIfNeeded() {
+        if (!isOrganizedTaskFragment() || mTransitionController.isCollecting(this)) {
+            return;
+        }
+        if (getChildCount() == 0) {
+            // The TaskFragment is new created, and just becoming non-empty.
+            mTransitionController.collectExistenceChange(this);
+        } else {
+            mTransitionController.collect(this);
+        }
+    }
+
     @Override
     void setSurfaceControl(SurfaceControl sc) {
         super.setSurfaceControl(sc);
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 91f69a5..31d8eb8 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1149,6 +1149,26 @@
         return false;
     }
 
+    private static boolean isTranslucent(@NonNull WindowContainer wc) {
+        final TaskFragment taskFragment = wc.asTaskFragment();
+        if (taskFragment != null) {
+            if (taskFragment.isTranslucent(null /* starting */)) {
+                return true;
+            }
+            final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
+            if (adjacentTaskFragment != null) {
+                // Treat the TaskFragment as translucent if its adjacent TF is, otherwise everything
+                // behind two adjacent TaskFragments are occluded.
+                return adjacentTaskFragment.isTranslucent(null /* starting */);
+            }
+        }
+        // TODO(b/172695805): hierarchical check. This is non-trivial because for containers
+        //                    it is effected by child visibility but needs to work even
+        //                    before visibility is committed. This means refactoring some
+        //                    checks to use requested visibility.
+        return !wc.fillsParent();
+    }
+
     /**
      * Under some conditions (eg. all visible targets within a parent container are transitioning
      * the same way) the transition can be "promoted" to the parent container. This means an
@@ -1701,20 +1721,13 @@
             if (mShowWallpaper || wc.showWallpaper()) {
                 flags |= FLAG_SHOW_WALLPAPER;
             }
-            if (!wc.fillsParent()) {
-                // TODO(b/172695805): hierarchical check. This is non-trivial because for containers
-                //                    it is effected by child visibility but needs to work even
-                //                    before visibility is committed. This means refactoring some
-                //                    checks to use requested visibility.
+            if (isTranslucent(wc)) {
                 flags |= FLAG_TRANSLUCENT;
             }
             final Task task = wc.asTask();
             if (task != null && task.voiceSession != null) {
                 flags |= FLAG_IS_VOICE_INTERACTION;
             }
-            if (task != null && task.isTranslucent(null)) {
-                flags |= FLAG_TRANSLUCENT;
-            }
             final ActivityRecord record = wc.asActivityRecord();
             if (record != null) {
                 if (record.mUseTransferredAnimation) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index d979753..0f5655c 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -21,6 +21,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
@@ -843,6 +844,8 @@
                     break;
                 }
 
+                prepareActivityEmbeddingTransitionForReparentActivityToTaskFragment(parent,
+                        activity);
                 activity.reparent(parent, POSITION_TOP);
                 effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 break;
@@ -1069,6 +1072,41 @@
         return effects;
     }
 
+    private void prepareActivityEmbeddingTransitionForReparentActivityToTaskFragment(
+            @NonNull TaskFragment taskFragment, @NonNull ActivityRecord activity) {
+        if (!taskFragment.canHaveEmbeddingActivityTransition(activity)) {
+            return;
+        }
+
+        // The reparent can happen in the following cases:
+        // 1. Reparent an existing activity to split when app launches new intent.
+        //    - This happens after app calls to start activity, but before the activity is actually
+        //      started, so we don't expect any collecting transition, but if it does, we can't
+        //      queue the WCT because the start activity won't wait.
+        // 2. Reparent an existing activity to split to launch placeholder when Task size changed.
+        //    - We expect to have a collecting transition for the Task resize, so just collect.
+        // 3. Reparent a new launching activity to an always-expand container.
+        // 4. Reparent a new launching activity to split to launch placeholder together.
+        // 5. Reparent a new launching activity to an existing split.
+        //    - The new launching activity should have start an OPEN transition, so just collect.
+        // 6. Reparent PiP activity back to the original Task.
+        //    - This should be part of the exiting PiP transition, so just collect.
+
+        if (!taskFragment.getBounds().equals(activity.getBounds()) && activity.isVisible()
+                && !mTransitionController.isCollecting()) {
+            // 1. Reparent an existing activity to split when app launches new intent.
+            mTransitionController.requestTransitionIfNeeded(TRANSIT_CHANGE, activity);
+        }
+
+        // We expect the activity to be in the transition already, so just collect the TaskFragment.
+        if (mTransitionController.isCollecting(activity)) {
+            taskFragment.collectEmbeddedTaskFragmentIfNeeded();
+        } else {
+            ProtoLog.w(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Reparenting Activity"
+                    + " to embedded TaskFragment, but the Activity is not collected");
+        }
+    }
+
     /** A helper method to send minimum dimension violation error to the client. */
     private void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions,
             IBinder errorCallbackToken, String reason) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index af8c4c8..5c9cb05 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -137,7 +137,6 @@
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
-import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowContainerChildProto.WINDOW;
@@ -2449,8 +2448,8 @@
 
         final DisplayContent dc = getDisplayContent();
         if (isImeLayeringTarget()) {
-            // Remove the IME screenshot surface if the layering target is not animating.
-            dc.removeImeScreenshotIfPossible();
+            // Remove the attached IME screenshot surface.
+            dc.removeImeSurfaceByTarget(this);
             // Make sure to set mImeLayeringTarget as null when the removed window is the
             // IME target, in case computeImeTarget may use the outdated target.
             dc.setImeLayeringTarget(null);
@@ -3584,6 +3583,7 @@
         } else {
             logExclusionRestrictions(EXCLUSION_LEFT);
             logExclusionRestrictions(EXCLUSION_RIGHT);
+            getDisplayContent().removeImeSurfaceByTarget(this);
         }
         // Exclude toast because legacy apps may show toast window by themselves, so the misused
         // apps won't always be considered as foreground state.
@@ -4970,10 +4970,6 @@
                 || isAnimating(0 /* flags */, ANIMATION_TYPE_WINDOW_ANIMATION);
     }
 
-    boolean isExitAnimationRunningSelfOrChild() {
-        return isAnimating(CHILDREN, ANIMATION_TYPE_WINDOW_ANIMATION);
-    }
-
     private boolean shouldFinishAnimatingExit() {
         // Exit animation might be applied soon.
         if (inTransition()) {
@@ -5927,6 +5923,10 @@
         if (!super.prepareSync()) {
             return false;
         }
+        if (mIsWallpaper) {
+            // TODO(b/233286785): Add sync support to wallpaper.
+            return false;
+        }
         // In the WindowContainer implementation we immediately mark ready
         // since a generic WindowContainer only needs to wait for its
         // children to finish and is immediately ready from its own
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 1a49f8a..ea1e49d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -43,8 +43,10 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 import android.testing.TestableContext;
 
@@ -128,6 +130,8 @@
     @Captor
     private ArgumentCaptor<Consumer<OperationContext>> mContextInjector;
 
+    private TestLooper mLooper = new TestLooper();
+
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
@@ -233,6 +237,9 @@
         client.start(mCallback);
         client.onAuthenticated(new Fingerprint("name", 2 /* enrollmentId */, SENSOR_ID),
                 true /* authenticated */, new ArrayList<>());
+
+        mLooper.moveTimeForward(10);
+        mLooper.dispatchAll();
         verify(mLuxProbe).destroy();
 
         client.onAcquired(2, 0);
@@ -309,9 +316,58 @@
         client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */, 2 /* deviceId */),
                 true /* authenticated */, new ArrayList<>());
 
+        mLooper.moveTimeForward(10);
+        mLooper.dispatchAll();
         verify(mCancellationSignal).cancel();
     }
 
+    @Test
+    public void fingerprintPowerIgnoresAuthInWindow() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        client.onPowerPressed();
+        client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */, 2 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        mLooper.moveTimeForward(1000);
+        mLooper.dispatchAll();
+
+        verify(mCallback).onClientFinished(any(), eq(false));
+    }
+
+    @Test
+    public void fingerprintAuthIgnoredWaitingForPower() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        client.onAuthenticated(new Fingerprint("friendly", 3 /* fingerId */, 4 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        client.onPowerPressed();
+        mLooper.moveTimeForward(1000);
+        mLooper.dispatchAll();
+
+        verify(mCallback).onClientFinished(any(), eq(false));
+    }
+
+    @Test
+    public void fingerprintAuthSucceedsAfterPowerWindow() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        client.onPowerPressed();
+        mLooper.moveTimeForward(1000);
+        mLooper.dispatchAll();
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        mLooper.moveTimeForward(1000);
+        mLooper.dispatchAll();
+
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
     private FingerprintAuthenticationClient createClient() throws RemoteException {
         return createClient(100 /* version */, true /* allowBackgroundAuthentication */);
     }
@@ -336,7 +392,8 @@
         9 /* sensorId */, mBiometricLogger, mBiometricContext,
         true /* isStrongBiometric */,
         null /* taskStackListener */, mLockoutCache,
-        mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication, mSensorProps) {
+        mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication, mSensorProps,
+                new Handler(mLooper.getLooper())) {
             @Override
             protected ActivityTaskManager getActivityTaskManager() {
                 return mActivityTaskManager;
diff --git a/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java b/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java
index 371861f..7746bd6 100644
--- a/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java
@@ -53,33 +53,30 @@
 
 /**
  * Unit tests for {@link SideFpsEventHandler}.
- * <p/>
- * Run with <code>atest SideFpsEventHandlerTest</code>.
+ *
+ * <p>Run with <code>atest SideFpsEventHandlerTest</code>.
  */
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class SideFpsEventHandlerTest {
 
-    private static final List<Integer> sAllStates = List.of(
-            BiometricStateListener.STATE_IDLE,
-            BiometricStateListener.STATE_ENROLLING,
-            BiometricStateListener.STATE_KEYGUARD_AUTH,
-            BiometricStateListener.STATE_BP_AUTH,
-            BiometricStateListener.STATE_AUTH_OTHER);
+    private static final List<Integer> sAllStates =
+            List.of(
+                    BiometricStateListener.STATE_IDLE,
+                    BiometricStateListener.STATE_ENROLLING,
+                    BiometricStateListener.STATE_KEYGUARD_AUTH,
+                    BiometricStateListener.STATE_BP_AUTH,
+                    BiometricStateListener.STATE_AUTH_OTHER);
 
     @Rule
     public TestableContext mContext =
             new TestableContext(InstrumentationRegistry.getContext(), null);
-    @Mock
-    private PackageManager mPackageManager;
-    @Mock
-    private FingerprintManager mFingerprintManager;
-    @Spy
-    private AlertDialog.Builder mDialogBuilder = new AlertDialog.Builder(mContext);
-    @Mock
-    private AlertDialog mAlertDialog;
-    @Mock
-    private Window mWindow;
+
+    @Mock private PackageManager mPackageManager;
+    @Mock private FingerprintManager mFingerprintManager;
+    @Spy private AlertDialog.Builder mDialogBuilder = new AlertDialog.Builder(mContext);
+    @Mock private AlertDialog mAlertDialog;
+    @Mock private Window mWindow;
 
     private TestLooper mLooper = new TestLooper();
     private SideFpsEventHandler mEventHandler;
@@ -95,9 +92,12 @@
         when(mDialogBuilder.create()).thenReturn(mAlertDialog);
         when(mAlertDialog.getWindow()).thenReturn(mWindow);
 
-        mEventHandler = new SideFpsEventHandler(
-                mContext, new Handler(mLooper.getLooper()),
-                mContext.getSystemService(PowerManager.class), () -> mDialogBuilder);
+        mEventHandler =
+                new SideFpsEventHandler(
+                        mContext,
+                        new Handler(mLooper.getLooper()),
+                        mContext.getSystemService(PowerManager.class),
+                        () -> mDialogBuilder);
     }
 
     @Test
@@ -105,7 +105,7 @@
         when(mPackageManager.hasSystemFeature(eq(PackageManager.FEATURE_FINGERPRINT)))
                 .thenReturn(false);
 
-        assertThat(mEventHandler.onSinglePressDetected(60L)).isFalse();
+        assertThat(mEventHandler.shouldConsumeSinglePress(60L)).isFalse();
 
         mLooper.dispatchAll();
         verify(mAlertDialog, never()).show();
@@ -117,7 +117,7 @@
 
         for (int state : sAllStates) {
             setBiometricState(state);
-            assertThat(mEventHandler.onSinglePressDetected(200L)).isFalse();
+            assertThat(mEventHandler.shouldConsumeSinglePress(200L)).isFalse();
 
             mLooper.dispatchAll();
             verify(mAlertDialog, never()).show();
@@ -130,7 +130,7 @@
 
         for (int state : sAllStates) {
             setBiometricState(state);
-            assertThat(mEventHandler.onSinglePressDetected(400L)).isFalse();
+            assertThat(mEventHandler.shouldConsumeSinglePress(400L)).isFalse();
 
             mLooper.dispatchAll();
             verify(mAlertDialog, never()).show();
@@ -139,13 +139,13 @@
 
     @Test
     public void ignoresWhenIdleOrUnknown() throws Exception {
-        setupWithSensor(true /* hasSfps */, true /* initialized */);
+        setupWithSensor(true /* hasSidefps */, true /* initialized */);
 
         setBiometricState(BiometricStateListener.STATE_IDLE);
-        assertThat(mEventHandler.onSinglePressDetected(80000L)).isFalse();
+        assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isFalse();
 
         setBiometricState(BiometricStateListener.STATE_AUTH_OTHER);
-        assertThat(mEventHandler.onSinglePressDetected(90000L)).isFalse();
+        assertThat(mEventHandler.shouldConsumeSinglePress(90000L)).isFalse();
 
         mLooper.dispatchAll();
         verify(mAlertDialog, never()).show();
@@ -156,7 +156,7 @@
         setupWithSensor(true /* hasSfps */, true /* initialized */);
 
         setBiometricState(BiometricStateListener.STATE_KEYGUARD_AUTH);
-        assertThat(mEventHandler.onSinglePressDetected(80000L)).isFalse();
+        assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isFalse();
 
         mLooper.dispatchAll();
         verify(mAlertDialog, never()).show();
@@ -164,13 +164,13 @@
 
     @Test
     public void promptsWhenBPisActive() throws Exception {
-        setupWithSensor(true /* hasSfps */, true /* initialized */);
+        setupWithSensor(true /* hasSideFps */, true /* initialized */);
 
         setBiometricState(BiometricStateListener.STATE_BP_AUTH);
-        assertThat(mEventHandler.onSinglePressDetected(80000L)).isTrue();
+        assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue();
 
         mLooper.dispatchAll();
-        verify(mAlertDialog).show();
+        verify(mAlertDialog, never()).show();
     }
 
     @Test
@@ -178,7 +178,57 @@
         setupWithSensor(true /* hasSfps */, true /* initialized */);
 
         setBiometricState(BiometricStateListener.STATE_ENROLLING);
-        assertThat(mEventHandler.onSinglePressDetected(80000L)).isTrue();
+        assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue();
+
+        mLooper.dispatchAll();
+        verify(mAlertDialog).show();
+        verify(mAlertDialog, never()).dismiss();
+    }
+
+    @Test
+    public void dismissesDialogOnTouchWhenEnrolling() throws Exception {
+        setupWithSensor(true /* hasSfps */, true /* initialized */);
+
+        setBiometricState(BiometricStateListener.STATE_ENROLLING);
+        when(mAlertDialog.isShowing()).thenReturn(true);
+        assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue();
+
+        mLooper.dispatchAll();
+        verify(mAlertDialog).show();
+
+        mBiometricStateListener.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
+        mLooper.moveTimeForward(10000);
+        mLooper.dispatchAll();
+
+        verify(mAlertDialog).dismiss();
+    }
+
+    @Test
+    public void dismissesDialogFailsWhenPowerPressedAndDialogShowing() throws Exception {
+        setupWithSensor(true /* hasSfps */, true /* initialized */);
+
+        setBiometricState(BiometricStateListener.STATE_ENROLLING);
+        when(mAlertDialog.isShowing()).thenReturn(true);
+        assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue();
+
+        mLooper.dispatchAll();
+        verify(mAlertDialog).show();
+
+        mBiometricStateListener.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
+        assertThat(mEventHandler.shouldConsumeSinglePress(60L)).isTrue();
+
+        mLooper.dispatchAll();
+        verify(mAlertDialog, never()).dismiss();
+    }
+
+    @Test
+    public void showDialogAfterTap() throws Exception {
+        setupWithSensor(true /* hasSfps */, true /* initialized */);
+
+        setBiometricState(BiometricStateListener.STATE_ENROLLING);
+        when(mAlertDialog.isShowing()).thenReturn(true);
+        mBiometricStateListener.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
+        assertThat(mEventHandler.shouldConsumeSinglePress(60L)).isTrue();
 
         mLooper.dispatchAll();
         verify(mAlertDialog).show();
@@ -201,11 +251,13 @@
                 ArgumentCaptor.forClass(IFingerprintAuthenticatorsRegisteredCallback.class);
         verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(fpCallbackCaptor.capture());
         if (initialized) {
-            fpCallbackCaptor.getValue().onAllAuthenticatorsRegistered(
-                    List.of(mock(FingerprintSensorPropertiesInternal.class)));
+            fpCallbackCaptor
+                    .getValue()
+                    .onAllAuthenticatorsRegistered(
+                            List.of(mock(FingerprintSensorPropertiesInternal.class)));
             if (hasSfps) {
-                ArgumentCaptor<BiometricStateListener> captor = ArgumentCaptor.forClass(
-                        BiometricStateListener.class);
+                ArgumentCaptor<BiometricStateListener> captor =
+                        ArgumentCaptor.forClass(BiometricStateListener.class);
                 verify(mFingerprintManager).registerBiometricStateListener(captor.capture());
                 mBiometricStateListener = captor.getValue();
             }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
index bd7186e..2f92260 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
@@ -18,7 +18,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -26,7 +25,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AlarmManager;
 import android.app.NotificationHistory;
 import android.app.NotificationHistory.HistoricalNotification;
 import android.content.Context;
@@ -60,8 +58,6 @@
     Handler mFileWriteHandler;
     @Mock
     Context mContext;
-    @Mock
-    AlarmManager mAlarmManager;
 
     NotificationHistoryDatabase mDataBase;
 
@@ -96,22 +92,16 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager);
         when(mContext.getUser()).thenReturn(getContext().getUser());
         when(mContext.getPackageName()).thenReturn(getContext().getPackageName());
 
         mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest");
 
-        mDataBase = new NotificationHistoryDatabase(mContext, mFileWriteHandler, mRootDir);
+        mDataBase = new NotificationHistoryDatabase(mFileWriteHandler, mRootDir);
         mDataBase.init();
     }
 
     @Test
-    public void testDeletionReceiver() {
-        verify(mContext, times(1)).registerReceiver(any(), any(), anyInt());
-    }
-
-    @Test
     public void testPrune() throws Exception {
         GregorianCalendar cal = new GregorianCalendar();
         cal.setTimeInMillis(10);
@@ -144,8 +134,6 @@
         mDataBase.prune(retainDays, cal.getTimeInMillis());
 
         assertThat(mDataBase.mHistoryFiles).containsExactlyElementsIn(expectedFiles);
-
-        verify(mAlarmManager, times(6)).setExactAndAllowWhileIdle(anyInt(), anyLong(), any());
     }
 
     @Test
@@ -412,15 +400,14 @@
         when(file.getName()).thenReturn("5");
         when(af.getBaseFile()).thenReturn(file);
 
-        wbr.run(5, af);
+        wbr.run(af);
 
         assertThat(mDataBase.mHistoryFiles.size()).isEqualTo(1);
         assertThat(mDataBase.mBuffer).isNotEqualTo(nh);
-        verify(mAlarmManager, times(1)).setExactAndAllowWhileIdle(anyInt(), anyLong(), any());
     }
 
     @Test
-    public void testRemoveFilePathFromHistory_hasMatch() throws Exception {
+    public void testRemoveFilePathFromHistory_hasMatch() {
         for (int i = 0; i < 5; i++) {
             AtomicFile af = mock(AtomicFile.class);
             when(af.getBaseFile()).thenReturn(new File(mRootDir, "af" + i));
@@ -436,7 +423,7 @@
     }
 
     @Test
-    public void testRemoveFilePathFromHistory_noMatch() throws Exception {
+    public void testRemoveFilePathFromHistory_noMatch() {
         for (int i = 0; i < 5; i++) {
             AtomicFile af = mock(AtomicFile.class);
             when(af.getBaseFile()).thenReturn(new File(mRootDir, "af" + i));
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java
new file mode 100644
index 0000000..af10b9d
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.server.notification;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.TestCase.assertFalse;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.app.job.JobServiceEngine;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.rule.ServiceTestRule;
+
+import com.android.server.LocalServices;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.lang.reflect.Field;
+
+@RunWith(AndroidTestingRunner.class)
+public class NotificationHistoryJobServiceTest extends UiServiceTestCase {
+    private NotificationHistoryJobService mJobService;
+    private JobParameters mJobParams = new JobParameters(null,
+            NotificationHistoryJobService.BASE_JOB_ID, null, null, null,
+            0, false, false, null, null, null);
+
+    @Captor
+    ArgumentCaptor<JobInfo> mJobInfoCaptor;
+
+    @Mock
+    private JobScheduler mMockJobScheduler;
+
+    @Mock
+    private NotificationManagerInternal mMockNotificationManagerInternal;
+
+    @Rule
+    public final ServiceTestRule mServiceRule = new ServiceTestRule();
+
+    @Before
+    public void setUp() throws Exception {
+        mJobService = new NotificationHistoryJobService();
+
+        final Field field = JobService.class.getDeclaredField("mEngine");
+        field.setAccessible(true);
+        field.set(mJobService, mock(JobServiceEngine.class));
+        mContext.addMockSystemService(JobScheduler.class, mMockJobScheduler);
+
+        // add NotificationManagerInternal to LocalServices
+        LocalServices.removeServiceForTest(NotificationManagerInternal.class);
+        LocalServices.addService(NotificationManagerInternal.class,
+                mMockNotificationManagerInternal);
+    }
+
+    @Test
+    public void testScheduleJob() {
+        // if asked, the job doesn't currently exist yet
+        when(mMockJobScheduler.getPendingJob(anyInt())).thenReturn(null);
+
+        // attempt to schedule the job
+        NotificationHistoryJobService.scheduleJob(mContext);
+        verify(mMockJobScheduler, times(1)).schedule(mJobInfoCaptor.capture());
+
+        // verify various properties of the job that is passed in to the job scheduler
+        JobInfo jobInfo = mJobInfoCaptor.getValue();
+        assertEquals(NotificationHistoryJobService.BASE_JOB_ID, jobInfo.getId());
+        assertFalse(jobInfo.isPersisted());
+        assertTrue(jobInfo.isPeriodic());
+    }
+
+    @Test
+    public void testOnStartJob() {
+        assertTrue(mJobService.onStartJob(mJobParams));
+
+        verify(mMockNotificationManagerInternal, timeout(500).atLeastOnce()).cleanupHistoryFiles();
+    }
+}
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index b1b323b..443ed58 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -8369,7 +8369,7 @@
     public void testOnUnlockUser() {
         UserInfo ui = new UserInfo();
         ui.id = 10;
-        mService.onUserUnlocking(new TargetUser(ui));
+        mService.onUserUnlocked(new TargetUser(ui));
         waitForIdle();
 
         verify(mHistoryManager, timeout(MAX_POST_DELAY).times(1)).onUserUnlocked(ui.id);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 1f07b20..d6b807f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2207,6 +2207,52 @@
         assertNotEquals(curSnapshot, mDisplayContent.mImeScreenshot);
     }
 
+    @UseTestDisplay(addWindows = {W_INPUT_METHOD})
+    @Test
+    public void testRemoveImeScreenshot_whenTargetSurfaceWasInvisible() {
+        final Task rootTask = createTask(mDisplayContent);
+        final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
+        final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
+        final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
+        win.onSurfaceShownChanged(true);
+        makeWindowVisible(win, mDisplayContent.mInputMethodWindow);
+        task.getDisplayContent().prepareAppTransition(TRANSIT_CLOSE);
+        doReturn(true).when(task).okToAnimate();
+        ArrayList<WindowContainer> sources = new ArrayList<>();
+        sources.add(activity);
+
+        mDisplayContent.setImeLayeringTarget(win);
+        mDisplayContent.setImeInputTarget(win);
+        mDisplayContent.getInsetsStateController().getImeSourceProvider().setImeShowing(true);
+        task.applyAnimation(null, TRANSIT_OLD_TASK_CLOSE, false /* enter */,
+                false /* isVoiceInteraction */, sources);
+        assertNotNull(mDisplayContent.mImeScreenshot);
+
+        win.onSurfaceShownChanged(false);
+        assertNull(mDisplayContent.mImeScreenshot);
+    }
+
+    @UseTestDisplay(addWindows = {W_INPUT_METHOD})
+    @Test
+    public void testRemoveImeScreenshot_whenWindowRemoveImmediately() {
+        final Task rootTask = createTask(mDisplayContent);
+        final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
+        final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
+        final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
+        makeWindowVisible(mDisplayContent.mInputMethodWindow);
+
+        mDisplayContent.setImeLayeringTarget(win);
+        mDisplayContent.setImeInputTarget(win);
+        mDisplayContent.getInsetsStateController().getImeSourceProvider().setImeShowing(true);
+        mDisplayContent.showImeScreenshot();
+        assertNotNull(mDisplayContent.mImeScreenshot);
+
+        // Expect IME snapshot will be removed when the win is IME layering target and invoked
+        // removeImeSurfaceByTarget.
+        win.removeImmediately();
+        assertNull(mDisplayContent.mImeScreenshot);
+    }
+
     @Test
     public void testRotateBounds_keepSamePhysicalPosition() {
         final DisplayContent dc =