Merge "Fixing Storage Volume(s) Retrieval."
diff --git a/Android.bp b/Android.bp
index aa65486..ed7a4813 100644
--- a/Android.bp
+++ b/Android.bp
@@ -150,6 +150,9 @@
     visibility: [
         // DO NOT ADD ANY MORE ENTRIES TO THIS LIST
         "//external/robolectric-shadows:__subpackages__",
+        //This will eventually replace the item above, and serves the
+        //same purpose.
+        "//external/robolectric:__subpackages__",
         "//frameworks/layoutlib:__subpackages__",
     ],
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java b/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java
new file mode 100644
index 0000000..412cb5a
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.libcore;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Xml;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.XmlObjectFactory;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Compares various kinds of method invocation.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class XmlSerializerPerfTest {
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void timeFastSerializer_nonIndent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = Xml.newFastSerializer();
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeFastSerializer_indent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = Xml.newFastSerializer();
+            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeKXmlSerializer_nonIndent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = XmlObjectFactory.newXmlSerializer();
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeKXmlSerializer_indent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = XmlObjectFactory.newXmlSerializer();
+            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            runTest(serializer, 100);
+        }
+    }
+
+    private void runTest(XmlSerializer serializer, int depth) throws IOException {
+        File file = File.createTempFile(XmlSerializerPerfTest.class.getSimpleName(), "tmp");
+        try (OutputStream out = new FileOutputStream(file)) {
+            serializer.setOutput(out, StandardCharsets.UTF_8.name());
+            serializer.startDocument(null, true);
+            writeContent(serializer, depth);
+            serializer.endDocument();
+        }
+    }
+
+    private void writeContent(XmlSerializer serializer, int depth) throws IOException {
+        serializer.startTag(null, "tag");
+        serializer.attribute(null, "attribute", "value1");
+        if (depth > 0) {
+            writeContent(serializer, depth - 1);
+        }
+        serializer.endTag(null, "tag");
+    }
+
+}
diff --git a/api/Android.bp b/api/Android.bp
index 9306671..a3e64a5 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -98,6 +98,7 @@
         "framework-configinfrastructure",
         "framework-connectivity",
         "framework-connectivity-t",
+        "framework-devicelock",
         "framework-federatedcompute",
         "framework-graphics",
         "framework-healthconnect",
diff --git a/boot/Android.bp b/boot/Android.bp
index 9fdb9bc..7839918 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -72,6 +72,10 @@
             module: "com.android.conscrypt-bootclasspath-fragment",
         },
         {
+            apex: "com.android.devicelock",
+            module: "com.android.devicelock-bootclasspath-fragment",
+        },
+        {
             apex: "com.android.federatedcompute",
             module: "com.android.federatedcompute-bootclasspath-fragment",
         },
diff --git a/core/api/current.txt b/core/api/current.txt
index cc14512..94d199c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -34,6 +34,7 @@
     field public static final String BIND_COMPANION_DEVICE_SERVICE = "android.permission.BIND_COMPANION_DEVICE_SERVICE";
     field public static final String BIND_CONDITION_PROVIDER_SERVICE = "android.permission.BIND_CONDITION_PROVIDER_SERVICE";
     field public static final String BIND_CONTROLS = "android.permission.BIND_CONTROLS";
+    field public static final String BIND_CREDENTIAL_PROVIDER_SERVICE = "android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE";
     field public static final String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN";
     field public static final String BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE";
     field public static final String BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE";
@@ -106,6 +107,7 @@
     field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK";
     field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS";
     field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE";
+    field public static final String MANAGE_DEVICE_LOCK_STATE = "android.permission.MANAGE_DEVICE_LOCK_STATE";
     field public static final String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS";
     field public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE";
     field public static final String MANAGE_MEDIA = "android.permission.MANAGE_MEDIA";
@@ -9844,6 +9846,7 @@
     field public static final int CONTEXT_RESTRICTED = 4; // 0x4
     field public static final String CREDENTIAL_SERVICE = "credential";
     field public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
+    field public static final String DEVICE_LOCK_SERVICE = "device_lock";
     field public static final String DEVICE_POLICY_SERVICE = "device_policy";
     field public static final String DISPLAY_HASH_SERVICE = "display_hash";
     field public static final String DISPLAY_SERVICE = "display";
@@ -11990,6 +11993,7 @@
     field public static final String FEATURE_CONTROLS = "android.software.controls";
     field public static final String FEATURE_CREDENTIALS = "android.software.credentials";
     field public static final String FEATURE_DEVICE_ADMIN = "android.software.device_admin";
+    field public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock";
     field public static final String FEATURE_EMBEDDED = "android.hardware.type.embedded";
     field public static final String FEATURE_ETHERNET = "android.hardware.ethernet";
     field public static final String FEATURE_EXPANDED_PICTURE_IN_PICTURE = "android.software.expanded_picture_in_picture";
@@ -39319,6 +39323,7 @@
     method public final void setNotificationsShown(String[]);
     method public final void snoozeNotification(String, long);
     method public final void updateNotificationChannel(@NonNull String, @NonNull android.os.UserHandle, @NonNull android.app.NotificationChannel);
+    field public static final String ACTION_SETTINGS_HOME = "android.service.notification.action.SETTINGS_HOME";
     field public static final int FLAG_FILTER_TYPE_ALERTING = 2; // 0x2
     field public static final int FLAG_FILTER_TYPE_CONVERSATIONS = 1; // 0x1
     field public static final int FLAG_FILTER_TYPE_ONGOING = 8; // 0x8
@@ -39326,7 +39331,6 @@
     field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4
     field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1
     field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2
-    field public static final String INTENT_CATEGORY_SETTINGS_HOME = "android.service.notification.category.SETTINGS_HOME";
     field public static final int INTERRUPTION_FILTER_ALARMS = 4; // 0x4
     field public static final int INTERRUPTION_FILTER_ALL = 1; // 0x1
     field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
@@ -41726,10 +41730,12 @@
     field public static final String KEY_OPPORTUNISTIC_NETWORK_PING_PONG_TIME_LONG = "opportunistic_network_ping_pong_time_long";
     field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool";
     field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
+    field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY = "premium_capability_maximum_notification_count_int_array";
     field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_notification_backoff_hysteresis_time_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG = "premium_capability_notification_display_timeout_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING = "premium_capability_purchase_url_string";
+    field public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL = "premium_capability_supported_on_lte_bool";
     field public static final String KEY_PREVENT_CLIR_ACTIVATION_AND_DEACTIVATION_CODE_BOOL = "prevent_clir_activation_and_deactivation_code_bool";
     field public static final String KEY_RADIO_RESTART_FAILURE_CAUSES_INT_ARRAY = "radio_restart_failure_causes_int_array";
     field public static final String KEY_RCS_CONFIG_SERVER_URL_STRING = "rcs_config_server_url_string";
@@ -41793,6 +41799,8 @@
     field public static final String KEY_VOICEMAIL_NOTIFICATION_PERSISTENT_BOOL = "voicemail_notification_persistent_bool";
     field public static final String KEY_VOICE_PRIVACY_DISABLE_UI_BOOL = "voice_privacy_disable_ui_bool";
     field public static final String KEY_VOLTE_REPLACEMENT_RAT_INT = "volte_replacement_rat_int";
+    field public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool";
+    field public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool";
     field public static final String KEY_VT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_RTT_CALL_BOOL = "vt_upgrade_supported_for_downgraded_rtt_call";
     field public static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = "vvm_cellular_data_required_bool";
     field public static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string";
@@ -44006,7 +44014,7 @@
     field public static final int PHONE_TYPE_GSM = 1; // 0x1
     field public static final int PHONE_TYPE_NONE = 0; // 0x0
     field public static final int PHONE_TYPE_SIP = 3; // 0x3
-    field public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1; // 0x1
+    field public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4; // 0x4
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED = 3; // 0x3
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED = 7; // 0x7
@@ -44014,12 +44022,13 @@
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; // 0xa
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; // 0xd
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; // 0xe
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; // 0x1
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED = 2; // 0x2
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT = 9; // 0x9
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED = 6; // 0x6
-    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5; // 0x5
     field public static final int SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION = 2; // 0x2
     field public static final int SET_OPPORTUNISTIC_SUB_NO_OPPORTUNISTIC_SUB_AVAILABLE = 3; // 0x3
     field public static final int SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION = 4; // 0x4
@@ -45401,7 +45410,7 @@
     method public final int getParagraphLeft(int);
     method public final int getParagraphRight(int);
     method public float getPrimaryHorizontal(int);
-    method @Nullable public android.util.Range<java.lang.Integer> getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy);
+    method @Nullable public int[] getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy);
     method public float getSecondaryHorizontal(int);
     method public void getSelectionPath(int, int, android.graphics.Path);
     method public final float getSpacingAdd();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index c170f74..b68e488 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -145,6 +145,7 @@
     field public static final String INTERACT_ACROSS_USERS_FULL = "android.permission.INTERACT_ACROSS_USERS_FULL";
     field public static final String INTERNAL_SYSTEM_WINDOW = "android.permission.INTERNAL_SYSTEM_WINDOW";
     field public static final String INVOKE_CARRIER_SETUP = "android.permission.INVOKE_CARRIER_SETUP";
+    field public static final String KILL_ALL_BACKGROUND_PROCESSES = "android.permission.KILL_ALL_BACKGROUND_PROCESSES";
     field public static final String KILL_UID = "android.permission.KILL_UID";
     field public static final String LAUNCH_DEVICE_MANAGER_SETUP = "android.permission.LAUNCH_DEVICE_MANAGER_SETUP";
     field public static final String LOCAL_MAC_ADDRESS = "android.permission.LOCAL_MAC_ADDRESS";
@@ -189,6 +190,7 @@
     field public static final String MANAGE_SOUND_TRIGGER = "android.permission.MANAGE_SOUND_TRIGGER";
     field public static final String MANAGE_SPEECH_RECOGNITION = "android.permission.MANAGE_SPEECH_RECOGNITION";
     field public static final String MANAGE_SUBSCRIPTION_PLANS = "android.permission.MANAGE_SUBSCRIPTION_PLANS";
+    field public static final String MANAGE_SUBSCRIPTION_USER_ASSOCIATION = "android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION";
     field public static final String MANAGE_TEST_NETWORKS = "android.permission.MANAGE_TEST_NETWORKS";
     field public static final String MANAGE_TIME_AND_ZONE_DETECTION = "android.permission.MANAGE_TIME_AND_ZONE_DETECTION";
     field public static final String MANAGE_UI_TRANSLATION = "android.permission.MANAGE_UI_TRANSLATION";
@@ -2502,11 +2504,49 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.app.time.ExternalTimeSuggestion> CREATOR;
   }
 
+  public final class TimeCapabilities implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getConfigureAutoDetectionEnabledCapability();
+    method public int getSetManualTimeCapability();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilities> CREATOR;
+  }
+
+  public final class TimeCapabilitiesAndConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.app.time.TimeCapabilities getCapabilities();
+    method @NonNull public android.app.time.TimeConfiguration getConfiguration();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilitiesAndConfig> CREATOR;
+  }
+
+  public final class TimeConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public boolean isAutoDetectionEnabled();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeConfiguration> CREATOR;
+  }
+
+  public static final class TimeConfiguration.Builder {
+    ctor public TimeConfiguration.Builder();
+    ctor public TimeConfiguration.Builder(@NonNull android.app.time.TimeConfiguration);
+    method @NonNull public android.app.time.TimeConfiguration build();
+    method @NonNull public android.app.time.TimeConfiguration.Builder setAutoDetectionEnabled(boolean);
+  }
+
   public final class TimeManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void addTimeZoneDetectorListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.time.TimeManager.TimeZoneDetectorListener);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTime(@NonNull android.app.time.UnixEpochTime);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTimeZone(@NonNull String);
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeState getTimeState();
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneState getTimeZoneState();
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void removeTimeZoneDetectorListener(@NonNull android.app.time.TimeManager.TimeZoneDetectorListener);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTime(@NonNull android.app.time.UnixEpochTime);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTimeZone(@NonNull String);
     method @RequiresPermission(android.Manifest.permission.SUGGEST_EXTERNAL_TIME) public void suggestExternalTime(@NonNull android.app.time.ExternalTimeSuggestion);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeConfiguration(@NonNull android.app.time.TimeConfiguration);
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeZoneConfiguration(@NonNull android.app.time.TimeZoneConfiguration);
   }
 
@@ -2514,10 +2554,19 @@
     method public void onChange();
   }
 
+  public final class TimeState implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.app.time.UnixEpochTime getUnixEpochTime();
+    method public boolean getUserShouldConfirmTime();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeState> CREATOR;
+  }
+
   public final class TimeZoneCapabilities implements android.os.Parcelable {
     method public int describeContents();
     method public int getConfigureAutoDetectionEnabledCapability();
     method public int getConfigureGeoDetectionEnabledCapability();
+    method public int getSetManualTimeZoneCapability();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneCapabilities> CREATOR;
   }
@@ -2546,6 +2595,24 @@
     method @NonNull public android.app.time.TimeZoneConfiguration.Builder setGeoDetectionEnabled(boolean);
   }
 
+  public final class TimeZoneState implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public String getId();
+    method public boolean getUserShouldConfirmId();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneState> CREATOR;
+  }
+
+  public final class UnixEpochTime implements android.os.Parcelable {
+    ctor public UnixEpochTime(long, long);
+    method @NonNull public android.app.time.UnixEpochTime at(long);
+    method public int describeContents();
+    method public long getElapsedRealtimeMillis();
+    method public long getUnixEpochTimeMillis();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.UnixEpochTime> CREATOR;
+  }
+
 }
 
 package android.app.usage {
@@ -13324,6 +13391,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int);
     method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int);
+    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getUserHandle(int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isSubscriptionEnabled(int);
     method public void requestEmbeddedSubscriptionInfoListRefresh();
     method public void requestEmbeddedSubscriptionInfoListRefresh(int);
@@ -13334,6 +13402,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setUserHandle(int, @Nullable android.os.UserHandle);
     field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED";
     field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI;
     field @NonNull public static final android.net.Uri CROSS_SIM_ENABLED_CONTENT_URI;
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1e4023e..186e5be 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -134,7 +134,7 @@
     method public static void resumeAppSwitches() throws android.os.RemoteException;
     method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void scheduleApplicationInfoChanged(java.util.List<java.lang.String>, int);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public void setStopUserOnSwitch(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int);
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int, boolean);
     method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle();
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 576b572..74329a3 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -3953,6 +3953,10 @@
      * processes to reclaim memory; the system will take care of restarting
      * these processes in the future as needed.
      *
+     * <p class="note">On devices with a {@link Build.VERSION#SECURITY_PATCH} of 2022-12-01 or
+     * greater, third party applications can only use this API to kill their own processes.
+     * </p>
+     *
      * @param packageName The name of the package whose processes are to
      * be killed.
      */
@@ -4401,7 +4405,7 @@
      */
     @TestApi
     @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
-            android.Manifest.permission.CREATE_USERS})
+            android.Manifest.permission.INTERACT_ACROSS_USERS})
     public boolean startUserInBackgroundOnSecondaryDisplay(@UserIdInt int userId,
             int displayId) {
         if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index c2df802..cc4650a7 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,6 +35,10 @@
 import android.os.PowerExemptionManager.ReasonCode;
 import android.os.PowerExemptionManager.TempAllowListType;
 
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /**
@@ -57,8 +62,11 @@
     private long mRequireCompatChangeId = CHANGE_INVALID;
     private boolean mRequireCompatChangeEnabled = true;
     private boolean mIsAlarmBroadcast = false;
+    private boolean mIsInteractiveBroadcast = false;
     private long mIdForResponseEvent;
     private @Nullable IntentFilter mRemoveMatchingFilter;
+    private @DeliveryGroupPolicy int mDeliveryGroupPolicy;
+    private @Nullable String mDeliveryGroupKey;
 
     /**
      * Change ID which is invalid.
@@ -161,6 +169,13 @@
             "android:broadcast.is_alarm";
 
     /**
+     * Corresponds to {@link #setInteractiveBroadcast(boolean)}
+     * @hide
+     */
+    public static final String KEY_INTERACTIVE_BROADCAST =
+            "android:broadcast.is_interactive";
+
+    /**
      * @hide
      * @deprecated Use {@link android.os.PowerExemptionManager#
      * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED} instead.
@@ -190,6 +205,46 @@
     private static final String KEY_REMOVE_MATCHING_FILTER =
             "android:broadcast.removeMatchingFilter";
 
+    /**
+     * Corresponds to {@link #setDeliveryGroupPolicy(int)}.
+     */
+    private static final String KEY_DELIVERY_GROUP_POLICY =
+            "android:broadcast.deliveryGroupPolicy";
+
+    /**
+     * Corresponds to {@link #setDeliveryGroupKey(String, String)}.
+     */
+    private static final String KEY_DELIVERY_GROUP_KEY =
+            "android:broadcast.deliveryGroupKey";
+
+    /**
+     * The list of delivery group policies which specify how multiple broadcasts belonging to
+     * the same delivery group has to be handled.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = {
+            DELIVERY_GROUP_POLICY_ALL,
+            DELIVERY_GROUP_POLICY_MOST_RECENT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeliveryGroupPolicy {}
+
+    /**
+     * Delivery group policy that indicates that all the broadcasts in the delivery group
+     * need to be delivered as is.
+     *
+     * @hide
+     */
+    public static final int DELIVERY_GROUP_POLICY_ALL = 0;
+
+    /**
+     * Delivery group policy that indicates that only the most recent broadcast in the delivery
+     * group need to be delivered and the rest can be dropped.
+     *
+     * @hide
+     */
+    public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1;
+
     public static BroadcastOptions makeBasic() {
         BroadcastOptions opts = new BroadcastOptions();
         return opts;
@@ -234,8 +289,12 @@
         mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true);
         mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT);
         mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false);
+        mIsInteractiveBroadcast = opts.getBoolean(KEY_INTERACTIVE_BROADCAST, false);
         mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER,
                 IntentFilter.class);
+        mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY,
+                DELIVERY_GROUP_POLICY_ALL);
+        mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY);
     }
 
     /**
@@ -549,6 +608,27 @@
     }
 
     /**
+     * When set, this broadcast will be understood as having originated from
+     * some direct interaction by the user such as a notification tap or button
+     * press.  Only the OS itself may use this option.
+     * @hide
+     * @param broadcastIsInteractive
+     * @see #isInteractiveBroadcast()
+     */
+    public void setInteractiveBroadcast(boolean broadcastIsInteractive) {
+        mIsInteractiveBroadcast = broadcastIsInteractive;
+    }
+
+    /**
+     * Did this broadcast originate with a direct user interaction?
+     * @return true if this broadcast is the result of an interaction, false otherwise
+     * @hide
+     */
+    public boolean isInteractiveBroadcast() {
+        return mIsInteractiveBroadcast;
+    }
+
+    /**
      * Did this broadcast originate from a push message from the server?
      *
      * @return true if this broadcast is a push message, false otherwise.
@@ -639,6 +719,41 @@
     }
 
     /**
+     * Set delivery group policy for this broadcast to specify how multiple broadcasts belonging to
+     * the same delivery group has to be handled.
+     *
+     * @hide
+     */
+    public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) {
+        mDeliveryGroupPolicy = policy;
+    }
+
+    /** @hide */
+    public @DeliveryGroupPolicy int getDeliveryGroupPolicy() {
+        return mDeliveryGroupPolicy;
+    }
+
+    /**
+     * Set namespace and key to identify the delivery group that this broadcast belongs to.
+     * If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be
+     * used to identify the delivery group.
+     *
+     * @hide
+     */
+    public void setDeliveryGroupKey(@NonNull String namespace, @NonNull String key) {
+        Preconditions.checkArgument(!namespace.contains("/"),
+                "namespace should not contain '/'");
+        Preconditions.checkArgument(!key.contains("/"),
+                "key should not contain '/'");
+        mDeliveryGroupKey = namespace + "/" + key;
+    }
+
+    /** @hide */
+    public String getDeliveryGroupKey() {
+        return mDeliveryGroupKey;
+    }
+
+    /**
      * Returns the created options as a Bundle, which can be passed to
      * {@link android.content.Context#sendBroadcast(android.content.Intent)
      * Context.sendBroadcast(Intent)} and related methods.
@@ -658,6 +773,9 @@
         if (mIsAlarmBroadcast) {
             b.putBoolean(KEY_ALARM_BROADCAST, true);
         }
+        if (mIsInteractiveBroadcast) {
+            b.putBoolean(KEY_INTERACTIVE_BROADCAST, true);
+        }
         if (mMinManifestReceiverApiLevel != 0) {
             b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel);
         }
@@ -686,6 +804,12 @@
         if (mRemoveMatchingFilter != null) {
             b.putParcelable(KEY_REMOVE_MATCHING_FILTER, mRemoveMatchingFilter);
         }
+        if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) {
+            b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy);
+        }
+        if (mDeliveryGroupKey != null) {
+            b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey);
+        }
         return b.isEmpty() ? null : b;
     }
 }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 74eb1c5..f9ef3cc 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -8467,8 +8467,8 @@
             }
 
             int maxAvatarSize = resources.getDimensionPixelSize(
-                    isLowRam ? R.dimen.notification_person_icon_max_size
-                            : R.dimen.notification_person_icon_max_size_low_ram);
+                    isLowRam ? R.dimen.notification_person_icon_max_size_low_ram
+                            : R.dimen.notification_person_icon_max_size);
             if (mUser != null && mUser.getIcon() != null) {
                 mUser.getIcon().scaleDownIfNecessary(maxAvatarSize, maxAvatarSize);
             }
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 4ddfdb6..08a6b8c 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -85,6 +85,7 @@
 import android.credentials.ICredentialManager;
 import android.debug.AdbManager;
 import android.debug.IAdbManager;
+import android.devicelock.DeviceLockFrameworkInitializer;
 import android.graphics.fonts.FontManager;
 import android.hardware.ConsumerIrManager;
 import android.hardware.ISerialManager;
@@ -1555,6 +1556,7 @@
             ConnectivityFrameworkInitializerTiramisu.registerServiceWrappers();
             NearbyFrameworkInitializer.registerServiceWrappers();
             OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers();
+            DeviceLockFrameworkInitializer.registerServiceWrappers();
         } finally {
             // If any of the above code throws, we're in a pretty bad shape and the process
             // will likely crash, but we'll reset it just in case there's an exception handler...
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index 88a7c0f..d2c7972 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -29,7 +29,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
@@ -1123,18 +1122,4 @@
             });
         }
     }
-
-    private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
-        final BackupManagerMonitor mMonitor;
-
-        BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
-            mMonitor = monitor;
-        }
-
-        @Override
-        public void onEvent(final Bundle event) throws RemoteException {
-            mMonitor.onEvent(event);
-        }
-    }
-
 }
diff --git a/core/java/android/app/backup/BackupManagerMonitorWrapper.java b/core/java/android/app/backup/BackupManagerMonitorWrapper.java
new file mode 100644
index 0000000..0b18995
--- /dev/null
+++ b/core/java/android/app/backup/BackupManagerMonitorWrapper.java
@@ -0,0 +1,41 @@
+/*
+ * 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.app.backup;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+/**
+ * Wrapper around {@link BackupManagerMonitor} that helps with IPC between the caller of backup
+ * APIs and the backup service.
+ *
+ * The caller implements {@link BackupManagerMonitor} and passes it into framework APIs that run on
+ * the caller's process. Those framework APIs will then wrap it around this class when doing the
+ * actual IPC.
+ */
+class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
+    private final BackupManagerMonitor mMonitor;
+
+    BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
+        mMonitor = monitor;
+    }
+
+    @Override
+    public void onEvent(final Bundle event) throws RemoteException {
+        mMonitor.onEvent(event);
+    }
+}
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java
new file mode 100644
index 0000000..b789b38
--- /dev/null
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.java
@@ -0,0 +1,280 @@
+/*
+ * 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.app.backup;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+// TODO(b/244436184): Make this @SystemApi
+/**
+ * Class to log B&R stats for each data type that is backed up and restored by the calling app.
+ *
+ * The logger instance is designed to accept a limited number of unique
+ * {link @BackupRestoreDataType} values, as determined by the underlying implementation. Apps are
+ * expected to have a small pre-defined set of data type values they use. Attempts to log too many
+ * unique values will be rejected.
+ *
+ * @hide
+ */
+public class BackupRestoreEventLogger {
+    /**
+     * Max number of unique data types for which an instance of this logger can store info. Attempts
+     * to use more distinct data type values will be rejected.
+     */
+    public static final int DATA_TYPES_ALLOWED = 15;
+
+    /**
+     * Operation types for which this logger can be used.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            OperationType.BACKUP,
+            OperationType.RESTORE
+    })
+    @interface OperationType {
+        int BACKUP = 1;
+        int RESTORE = 2;
+    }
+
+    /**
+     * Denotes that the annotated element identifies a data type as required by the logging methods
+     * of {@code BackupRestoreEventLogger}
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BackupRestoreDataType {}
+
+    /**
+     * Denotes that the annotated element identifies an error type as required by the logging
+     * methods of {@code BackupRestoreEventLogger}
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BackupRestoreError {}
+
+    private final int mOperationType;
+
+    /**
+     * @param operationType type of the operation for which logging will be performed. See
+     *                      {@link OperationType}. Attempts to use logging methods that don't match
+     *                      the specified operation type will be rejected (e.g. use backup methods
+     *                      for a restore logger and vice versa).
+     */
+    public BackupRestoreEventLogger(@OperationType int operationType) {
+        mOperationType = operationType;
+    }
+
+    /**
+     * Report progress during a backup operation. Call this method for each distinct data type that
+     * your {@code BackupAgent} implementation handles for any items of that type that have been
+     * successfully backed up. Repeated calls to this method with the same {@code dataType} will
+     * increase the total count of items associated with this data type by {@code count}.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed.
+     * @param count number of items of the given type that have been successfully backed up.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) {
+        return true;
+    }
+
+    /**
+     * Report errors during a backup operation. Call this method whenever items of a certain data
+     * type failed to back up. Repeated calls to this method with the same {@code dataType} /
+     * {@code error} will increase the total count of items associated with this data type / error
+     * by {@code count}.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed.
+     * @param count number of items of the given type that have failed to back up.
+     * @param error optional, the error that has caused the failure.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        return true;
+    }
+
+    /**
+     * Report metadata associated with a data type that is currently being backed up, e.g. name of
+     * the selected wallpaper file / package. Repeated calls to this method with the same {@code
+     * dataType} will overwrite the previously supplied {@code metaData} value.
+     *
+     * The logger does not store or transmit the provided metadata value. Instead, it’s replaced
+     * with the SHA-256 hash of the provided string.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed up.
+     * @param metaData the metadata associated with the data type.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logBackupMetaData(@NonNull @BackupRestoreDataType String dataType,
+            @NonNull String metaData) {
+        return true;
+    }
+
+    /**
+     * Report progress during a restore operation. Call this method for each distinct data type that
+     * your {@code BackupAgent} implementation handles if any items of that type have been
+     * successfully restored. Repeated calls to this method with the same {@code dataType} will
+     * increase the total count of items associated with this data type by {@code count}.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param count number of items of the given type that have been successfully restored.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) {
+        return true;
+    }
+
+    /**
+     * Report errors during a restore operation. Call this method whenever items of a certain data
+     * type failed to restore. Repeated calls to this method with the same {@code dataType} /
+     * {@code error} will increase the total count of items associated with this data type / error
+     * by {@code count}.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param count number of items of the given type that have failed to restore.
+     * @param error optional, the error that has caused the failure.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        return true;
+    }
+
+    /**
+     * Report metadata associated with a data type that is currently being restored, e.g. name of
+     * the selected wallpaper file / package. Repeated calls to this method with the same
+     * {@code dataType} will overwrite the previously supplied {@code metaData} value.
+     *
+     * The logger does not store or transmit the provided metadata value. Instead, it’s replaced
+     * with the SHA-256 hash of the provided string.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param metadata the metadata associated with the data type.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType,
+            @NonNull  String metadata) {
+        return true;
+    }
+
+    /**
+     * Get the contents of this logger. This method should only be used by B&R code in Android
+     * Framework.
+     *
+     * @hide
+     */
+    public List<DataTypeResult> getLoggingResults() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Get the operation type for which this logger was created. This method should only be used
+     * by B&R code in Android Framework.
+     *
+     * @hide
+     */
+    public @OperationType int getOperationType() {
+        return mOperationType;
+    }
+
+    /**
+     * Encapsulate logging results for a single data type.
+     */
+    public static class DataTypeResult {
+        @BackupRestoreDataType
+        private final String mDataType;
+        private final int mSuccessCount;
+        private final Map<String, Integer> mErrors;
+        private final byte[] mMetadataHash;
+
+        public DataTypeResult(String dataType, int successCount,
+                Map<String, Integer> errors, byte[] metadataHash) {
+            mDataType = dataType;
+            mSuccessCount = successCount;
+            mErrors = errors;
+            mMetadataHash = metadataHash;
+        }
+
+        @NonNull
+        @BackupRestoreDataType
+        public String getDataType() {
+            return mDataType;
+        }
+
+        /**
+         * @return number of items of the given data type that have been successfully backed up or
+         *         restored.
+         */
+        public int getSuccessCount() {
+            return mSuccessCount;
+        }
+
+        /**
+         * @return mapping of {@link BackupRestoreError} to the count of items that are affected by
+         *         the error.
+         */
+        @NonNull
+        public Map<String, Integer> getErrors() {
+            return mErrors;
+        }
+
+        /**
+         * @return SHA-256 hash of the metadata or {@code null} of no metadata has been logged for
+         *         this data type.
+         */
+        @Nullable
+        public byte[] getMetadataHash() {
+            return mMetadataHash;
+        }
+    }
+}
diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java
index f6de72b..90e9df4 100644
--- a/core/java/android/app/backup/BackupTransport.java
+++ b/core/java/android/app/backup/BackupTransport.java
@@ -656,6 +656,20 @@
     }
 
     /**
+     * Ask the transport for a {@link IBackupManagerMonitor} instance which will be used by the
+     * framework to report logging events back to the transport.
+     *
+     * <p>Backups requested from outside the framework may pass in a monitor with the request,
+     * however backups initiated by the framework will call this method to retrieve one.
+     *
+     * @hide
+     */
+    @Nullable
+    public BackupManagerMonitor getBackupManagerMonitor() {
+        return null;
+    }
+
+    /**
      * Bridge between the actual IBackupTransport implementation and the stable API.  If the
      * binder interface needs to change, we use this layer to translate so that we can
      * (if appropriate) decouple those framework-side changes from the BackupTransport
@@ -952,5 +966,15 @@
                 callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
             }
         }
+
+        @Override
+        public void getBackupManagerMonitor(AndroidFuture<IBackupManagerMonitor> resultFuture) {
+            try {
+                BackupManagerMonitor result = BackupTransport.this.getBackupManagerMonitor();
+                resultFuture.complete(new BackupManagerMonitorWrapper(result));
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
+        }
     }
 }
diff --git a/core/java/android/app/backup/RestoreSession.java b/core/java/android/app/backup/RestoreSession.java
index 9336704..fe68ec1 100644
--- a/core/java/android/app/backup/RestoreSession.java
+++ b/core/java/android/app/backup/RestoreSession.java
@@ -20,7 +20,6 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.content.Context;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
@@ -393,17 +392,4 @@
                     mHandler.obtainMessage(MSG_RESTORE_FINISHED, error, 0));
         }
     }
-
-    private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
-        final BackupManagerMonitor mMonitor;
-
-        BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
-            mMonitor = monitor;
-        }
-
-        @Override
-        public void onEvent(final Bundle event) throws RemoteException {
-            mMonitor.onEvent(event);
-        }
-    }
 }
diff --git a/core/java/android/app/time/TimeCapabilities.java b/core/java/android/app/time/TimeCapabilities.java
index 76bad58..752caac 100644
--- a/core/java/android/app/time/TimeCapabilities.java
+++ b/core/java/android/app/time/TimeCapabilities.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.app.time.Capabilities.CapabilityState;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -37,6 +38,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeCapabilities implements Parcelable {
 
     public static final @NonNull Creator<TimeCapabilities> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/TimeCapabilitiesAndConfig.java b/core/java/android/app/time/TimeCapabilitiesAndConfig.java
index b6a0818..c9a45e0 100644
--- a/core/java/android/app/time/TimeCapabilitiesAndConfig.java
+++ b/core/java/android/app/time/TimeCapabilitiesAndConfig.java
@@ -17,6 +17,7 @@
 package android.app.time;
 
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -27,6 +28,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeCapabilitiesAndConfig implements Parcelable {
 
     public static final @NonNull Creator<TimeCapabilitiesAndConfig> CREATOR =
diff --git a/core/java/android/app/time/TimeConfiguration.java b/core/java/android/app/time/TimeConfiguration.java
index 7d98698..048f85a 100644
--- a/core/java/android/app/time/TimeConfiguration.java
+++ b/core/java/android/app/time/TimeConfiguration.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.StringDef;
+import android.annotation.SystemApi;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -40,6 +41,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeConfiguration implements Parcelable {
 
     public static final @NonNull Creator<TimeConfiguration> CREATOR =
@@ -155,6 +157,7 @@
      *
      * @hide
      */
+    @SystemApi
     public static final class Builder {
 
         private final Bundle mBundle = new Bundle();
diff --git a/core/java/android/app/time/TimeManager.java b/core/java/android/app/time/TimeManager.java
index 9f66f09..e35e359 100644
--- a/core/java/android/app/time/TimeManager.java
+++ b/core/java/android/app/time/TimeManager.java
@@ -88,8 +88,6 @@
 
     /**
      * Returns the calling user's time capabilities and configuration.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -107,10 +105,26 @@
     /**
      * Modifies the time detection configuration.
      *
-     * @return {@code true} if all the configuration settings specified have been set to the
-     * new values, {@code false} if none have
+     * <p>The ability to modify configuration settings can be subject to restrictions. For
+     * example, they may be determined by device hardware, general policy (i.e. only the primary
+     * user can set them), or by a managed device policy. Use {@link
+     * #getTimeCapabilitiesAndConfig()} to obtain information at runtime about the user's
+     * capabilities.
      *
-     * @hide
+     * <p>Attempts to modify configuration settings with capabilities that are {@link
+     * Capabilities#CAPABILITY_NOT_SUPPORTED} or {@link
+     * Capabilities#CAPABILITY_NOT_ALLOWED} will have no effect and a {@code false}
+     * will be returned. Modifying configuration settings with capabilities that are {@link
+     * Capabilities#CAPABILITY_NOT_APPLICABLE} or {@link
+     * Capabilities#CAPABILITY_POSSESSED} will succeed. See {@link
+     * TimeZoneCapabilities} for further details.
+     *
+     * <p>If the supplied configuration only has some values set, then only the specified settings
+     * will be updated (where the user's capabilities allow) and other settings will be left
+     * unchanged.
+     *
+     * @return {@code true} if all the configuration settings specified have been set to the
+     *   new values, {@code false} if none have
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean updateTimeConfiguration(@NonNull TimeConfiguration configuration) {
@@ -280,8 +294,6 @@
     /**
      * Returns a snapshot of the device's current system clock time state. See also {@link
      * #confirmTime(UnixEpochTime)} for how this information can be used.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -306,8 +318,6 @@
      * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time being
      * confirmed is no longer the time the device is currently set to. Confirming a time
      * in which the system already has high confidence will return {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean confirmTime(@NonNull UnixEpochTime unixEpochTime) {
@@ -329,8 +339,6 @@
      * capabilities prevents the time being accepted, e.g. if the device is currently set to
      * "automatic time detection". This method returns {@code true} if the time was accepted even
      * if it is the same as the current device time.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean setManualTime(@NonNull UnixEpochTime unixEpochTime) {
@@ -353,8 +361,6 @@
      * Returns a snapshot of the device's current time zone state. See also {@link
      * #confirmTimeZone(String)} and {@link #setManualTimeZone(String)} for how this information may
      * be used.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -379,8 +385,6 @@
      * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time zone ID being
      * confirmed is no longer the time zone ID the device is currently set to. Confirming a time
      * zone ID in which the system already has high confidence returns {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean confirmTimeZone(@NonNull String timeZoneId) {
@@ -402,8 +406,6 @@
      * capabilities prevents the time zone being accepted, e.g. if the device is currently set to
      * "automatic time zone detection". {@code true} is returned if the time zone is accepted. A
      * time zone that is accepted and matches the current device time zone returns {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean setManualTimeZone(@NonNull String timeZoneId) {
diff --git a/core/java/android/app/time/TimeState.java b/core/java/android/app/time/TimeState.java
index 01c869d..c209cde 100644
--- a/core/java/android/app/time/TimeState.java
+++ b/core/java/android/app/time/TimeState.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -36,6 +37,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeState implements Parcelable {
 
     public static final @NonNull Creator<TimeState> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java
index 2f147ce..b647fc3 100644
--- a/core/java/android/app/time/TimeZoneCapabilities.java
+++ b/core/java/android/app/time/TimeZoneCapabilities.java
@@ -114,8 +114,6 @@
      * <p>The time zone will be ignored in all cases unless the value is {@link
      * Capabilities#CAPABILITY_POSSESSED}. See also
      * {@link TimeZoneConfiguration#isAutoDetectionEnabled()}.
-     *
-     * @hide
      */
     @CapabilityState
     public int getSetManualTimeZoneCapability() {
diff --git a/core/java/android/app/time/TimeZoneState.java b/core/java/android/app/time/TimeZoneState.java
index 8e87111..beb6dc6 100644
--- a/core/java/android/app/time/TimeZoneState.java
+++ b/core/java/android/app/time/TimeZoneState.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -36,6 +37,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeZoneState implements Parcelable {
 
     public static final @NonNull Creator<TimeZoneState> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/UnixEpochTime.java b/core/java/android/app/time/UnixEpochTime.java
index 576bf64..3a35f3c 100644
--- a/core/java/android/app/time/UnixEpochTime.java
+++ b/core/java/android/app/time/UnixEpochTime.java
@@ -19,6 +19,7 @@
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -38,6 +39,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class UnixEpochTime implements Parcelable {
     @ElapsedRealtimeLong private final long mElapsedRealtimeMillis;
     private final long mUnixEpochTimeMillis;
@@ -153,9 +155,8 @@
      * Creates a new Unix epoch time value at {@code elapsedRealtimeTimeMillis} by adjusting this
      * Unix epoch time by the difference between the elapsed realtime value supplied and the one
      * associated with this instance.
-     *
-     * @hide
      */
+    @NonNull
     public UnixEpochTime at(@ElapsedRealtimeLong long elapsedRealtimeTimeMillis) {
         long adjustedUnixEpochTimeMillis =
                 (elapsedRealtimeTimeMillis - mElapsedRealtimeMillis) + mUnixEpochTimeMillis;
diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java
index cc303fb..2dced96 100644
--- a/core/java/android/appwidget/AppWidgetHost.java
+++ b/core/java/android/appwidget/AppWidgetHost.java
@@ -329,6 +329,22 @@
     }
 
     /**
+     * Set the visibiity of all widgets associated with this host to hidden
+     *
+     * @hide
+     */
+    public void setAppWidgetHidden() {
+        if (sService == null) {
+            return;
+        }
+        try {
+            sService.setAppWidgetHidden(mContextOpPackageName, mHostId);
+        } catch (RemoteException e) {
+            throw new RuntimeException("System server dead?", e);
+        }
+    }
+
+    /**
      * Set the host's interaction handler.
      *
      * @hide
@@ -418,14 +434,7 @@
         AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
         view.setInteractionHandler(mInteractionHandler);
         view.setAppWidget(appWidgetId, appWidget);
-        addListener(appWidgetId, view);
-        RemoteViews views;
-        try {
-            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
-        } catch (RemoteException e) {
-            throw new RuntimeException("system server dead?", e);
-        }
-        view.updateAppWidget(views);
+        setListener(appWidgetId, view);
 
         return view;
     }
@@ -513,13 +522,19 @@
      * The AppWidgetHost retains a pointer to the newly-created listener.
      * @param appWidgetId The ID of the app widget for which to add the listener
      * @param listener The listener interface that deals with actions towards the widget view
-     *
      * @hide
      */
-    public void addListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
+    public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
         synchronized (mListeners) {
             mListeners.put(appWidgetId, listener);
         }
+        RemoteViews views = null;
+        try {
+            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
+        } catch (RemoteException e) {
+            throw new RuntimeException("system server dead?", e);
+        }
+        listener.updateAppWidget(views);
     }
 
     /**
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index e7f19166..295d69d 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -88,6 +88,7 @@
             IBinder token,
             in Point screenSize);
     void unregisterInputDevice(IBinder token);
+    int getInputDeviceId(IBinder token);
     boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
     boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
     boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 753c936..d65210b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3938,6 +3938,7 @@
             //@hide: SAFETY_CENTER_SERVICE,
             DISPLAY_HASH_SERVICE,
             CREDENTIAL_SERVICE,
+            DEVICE_LOCK_SERVICE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ServiceName {}
@@ -6073,6 +6074,14 @@
     public static final String CREDENTIAL_SERVICE = "credential";
 
     /**
+     * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.devicelock.DeviceLockManager}.
+     *
+     * @see #getSystemService(String)
+     */
+    public static final String DEVICE_LOCK_SERVICE = "device_lock";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index db991dc..823c142 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -4194,6 +4194,14 @@
     @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_CREDENTIALS = "android.software.credentials";
 
+    /**
+     * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
+     * The device supports locking (for example, by a financing provider in case of a missed
+     * payment).
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock";
+
     /** @hide */
     public static final boolean APP_ENUMERATION_ENABLED_BY_DEFAULT = true;
 
diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java
new file mode 100644
index 0000000..aeeede7
--- /dev/null
+++ b/core/java/android/credentials/ui/Constants.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+package android.credentials.ui;
+
+/**
+ * Constants for the ui protocol that doesn't fit into other individual data structures.
+ *
+ * @hide
+ */
+public class Constants {
+
+    /**
+    * The intent extra key for the {@code ResultReceiver} object when launching the UX
+    * activities.
+    */
+    public static final String EXTRA_RESULT_RECEIVER =
+            "android.credentials.ui.extra.RESULT_RECEIVER";
+
+}
diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java
index 122c54a..b9ee72d 100644
--- a/core/java/android/credentials/ui/Entry.java
+++ b/core/java/android/credentials/ui/Entry.java
@@ -30,12 +30,39 @@
  * @hide
  */
 public class Entry implements Parcelable {
-    // TODO: move to jetpack.
+    // TODO: these constants should go to jetpack.
     public static final String VERSION = "v1";
     public static final Uri CREDENTIAL_MANAGER_ENTRY_URI = Uri.parse("credentialmanager.slice");
-    public static final String HINT_TITLE = "hint_title";
-    public static final String HINT_SUBTITLE = "hint_subtitle";
-    public static final String HINT_ICON = "hint_icon";
+    // TODO: remove these hint constants and use the credential entry & action ones defined below.
+    public static final String HINT_TITLE = "HINT_TITLE";
+    public static final String HINT_SUBTITLE = "HINT_SUBTITLE";
+    public static final String HINT_ICON = "HINT_ICON";
+    /**
+     * 1. CREDENTIAL ENTRY CONSTANTS
+     */
+    // User profile picture associated with this credential entry.
+    public static final String HINT_PROFILE_ICON = "HINT_PROFILE_ICON";
+    public static final String HINT_CREDENTIAL_TYPE_ICON = "HINT_CREDENTIAL_TYPE_ICON";
+     // The user account name of this provider app associated with this entry.
+     // Note: this is independent from the request app.
+    public static final String HINT_USER_PROVIDER_ACCOUNT_NAME = "HINT_USER_PROVIDER_ACCOUNT_NAME";
+    public static final String HINT_PASSWORD_COUNT = "HINT_PASSWORD_COUNT";
+    public static final String HINT_PASSKEY_COUNT = "HINT_PASSKEY_COUNT";
+    public static final String HINT_TOTAL_CREDENTIAL_COUNT = "HINT_TOTAL_CREDENTIAL_COUNT";
+    public static final String HINT_LAST_USED_TIME_MILLIS = "HINT_LAST_USED_TIME_MILLIS";
+    /** Below are only available for get flows. */
+    public static final String HINT_NOTE = "HINT_NOTE";
+    public static final String HINT_USER_NAME = "HINT_USER_NAME";
+    public static final String HINT_CREDENTIAL_TYPE = "HINT_CREDENTIAL_TYPE";
+    public static final String HINT_PASSKEY_USER_DISPLAY_NAME = "HINT_PASSKEY_USER_DISPLAY_NAME";
+    public static final String HINT_PASSWORD_VALUE = "HINT_PASSWORD_VALUE";
+
+    /**
+     * 2. ACTION CONSTANTS
+     */
+    public static final String HINT_ACTION_TITLE = "HINT_ACTION_TITLE";
+    public static final String HINT_ACTION_SUBTEXT = "HINT_ACTION_SUBTEXT";
+    public static final String HINT_ACTION_ICON = "HINT_ACTION_ICON";
 
     /**
     * The intent extra key for the action chip {@code Entry} list when launching the UX activities.
@@ -55,7 +82,7 @@
     public static final String EXTRA_ENTRY_AUTHENTICATION_ACTION =
             "android.credentials.ui.extra.ENTRY_AUTHENTICATION_ACTION";
 
-    // TODO: may be changed to other type depending on the service implementation.
+    // TODO: change to string key + string subkey.
     private final int mId;
 
     @NonNull
diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java
new file mode 100644
index 0000000..1b70ea4
--- /dev/null
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+package android.credentials.ui;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+
+import java.util.ArrayList;
+
+/**
+ * Helpers for generating the intents and related extras parameters to launch the UI activities.
+ *
+ * @hide
+ */
+public class IntentFactory {
+    /** Generate a new launch intent to the . */
+    public static Intent newIntent(RequestInfo requestInfo,
+            ArrayList<ProviderData> providerDataList, ResultReceiver resultReceiver) {
+        Intent intent = new Intent();
+        // TODO: define these as proper config strings.
+        String activityName = "com.android.credentialmanager/.CredentialSelectorActivity";
+        intent.setComponent(ComponentName.unflattenFromString(activityName));
+
+        intent.putParcelableArrayListExtra(
+                ProviderData.EXTRA_PROVIDER_DATA_LIST, providerDataList);
+        intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
+        intent.putExtra(Constants.EXTRA_RESULT_RECEIVER,
+                toIpcFriendlyResultReceiver(resultReceiver));
+
+        return intent;
+    }
+
+    /**
+    * Convert an instance of a "locally-defined" ResultReceiver to an instance of
+    * {@link android.os.ResultReceiver} itself, which the receiving process will be able to
+    * unmarshall.
+    */
+    private static <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver(
+            T resultReceiver) {
+        final Parcel parcel = Parcel.obtain();
+        resultReceiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        return ipcFriendly;
+    }
+
+    private IntentFactory() {}
+}
diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java
index 38bd4e5..35e12fa 100644
--- a/core/java/android/credentials/ui/ProviderData.java
+++ b/core/java/android/credentials/ui/ProviderData.java
@@ -16,8 +16,10 @@
 
 package android.credentials.ui;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -43,30 +45,49 @@
     @NonNull
     private final String mProviderId;
     @NonNull
+    private final String mProviderDisplayName;
+    @NonNull
+    private final Icon mIcon;
+    @NonNull
     private final List<Entry> mCredentialEntries;
     @NonNull
     private final List<Entry> mActionChips;
     @Nullable
     private final Entry mAuthenticationEntry;
 
+    private final @CurrentTimeMillisLong long mLastUsedTimeMillis;
+
     public ProviderData(
-            @NonNull String providerId,
-            @NonNull List<Entry> credentialEntries,
-            @NonNull List<Entry> actionChips,
-            @Nullable Entry authenticationEntry) {
+            @NonNull String providerId, @NonNull String providerDisplayName,
+            @NonNull Icon icon, @NonNull List<Entry> credentialEntries,
+            @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry,
+            @CurrentTimeMillisLong long lastUsedTimeMillis) {
         mProviderId = providerId;
+        mProviderDisplayName = providerDisplayName;
+        mIcon = icon;
         mCredentialEntries = credentialEntries;
         mActionChips = actionChips;
         mAuthenticationEntry = authenticationEntry;
+        mLastUsedTimeMillis = lastUsedTimeMillis;
     }
 
-    /** Returns the provider package name. */
+    /** Returns the unique provider id. */
     @NonNull
     public String getProviderId() {
         return mProviderId;
     }
 
     @NonNull
+    public String getProviderDisplayName() {
+        return mProviderDisplayName;
+    }
+
+    @NonNull
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    @NonNull
     public List<Entry> getCredentialEntries() {
         return mCredentialEntries;
     }
@@ -81,11 +102,24 @@
         return mAuthenticationEntry;
     }
 
+    /** Returns the time when the provider was last used. */
+    public @CurrentTimeMillisLong long getLastUsedTimeMillis() {
+        return mLastUsedTimeMillis;
+    }
+
     protected ProviderData(@NonNull Parcel in) {
         String providerId = in.readString8();
         mProviderId = providerId;
         AnnotationValidations.validate(NonNull.class, null, mProviderId);
 
+        String providerDisplayName = in.readString8();
+        mProviderDisplayName = providerDisplayName;
+        AnnotationValidations.validate(NonNull.class, null, mProviderDisplayName);
+
+        Icon icon = in.readTypedObject(Icon.CREATOR);
+        mIcon = icon;
+        AnnotationValidations.validate(NonNull.class, null, mIcon);
+
         List<Entry> credentialEntries = new ArrayList<>();
         in.readTypedList(credentialEntries, Entry.CREATOR);
         mCredentialEntries = credentialEntries;
@@ -98,14 +132,20 @@
 
         Entry authenticationEntry = in.readTypedObject(Entry.CREATOR);
         mAuthenticationEntry = authenticationEntry;
+
+        long lastUsedTimeMillis = in.readLong();
+        mLastUsedTimeMillis = lastUsedTimeMillis;
     }
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mProviderId);
+        dest.writeString8(mProviderDisplayName);
+        dest.writeTypedObject(mIcon, flags);
         dest.writeTypedList(mCredentialEntries);
         dest.writeTypedList(mActionChips);
         dest.writeTypedObject(mAuthenticationEntry, flags);
+        dest.writeLong(mLastUsedTimeMillis);
     }
 
     @Override
@@ -124,4 +164,83 @@
             return new ProviderData[size];
         }
     };
+
+    /**
+     * Builder for {@link ProviderData}.
+     *
+     * @hide
+     */
+    public static class Builder {
+        private @NonNull String mProviderId;
+        private @NonNull String mProviderDisplayName;
+        private @NonNull Icon mIcon;
+        private @NonNull List<Entry> mCredentialEntries = new ArrayList<>();
+        private @NonNull List<Entry> mActionChips = new ArrayList<>();
+        private @Nullable Entry mAuthenticationEntry = null;
+        private @CurrentTimeMillisLong long mLastUsedTimeMillis = 0L;
+
+        /** Constructor with required properties. */
+        public Builder(@NonNull String providerId, @NonNull String providerDisplayName,
+                @NonNull Icon icon) {
+            mProviderId = providerId;
+            mProviderDisplayName = providerDisplayName;
+            mIcon = icon;
+        }
+
+        /** Sets the unique provider id. */
+        @NonNull
+        public Builder setProviderId(@NonNull String providerId) {
+            mProviderId = providerId;
+            return this;
+        }
+
+        /** Sets the provider display name to be displayed to the user. */
+        @NonNull
+        public Builder setProviderDisplayName(@NonNull String providerDisplayName) {
+            mProviderDisplayName = providerDisplayName;
+            return this;
+        }
+
+        /** Sets the provider icon to be displayed to the user. */
+        @NonNull
+        public Builder setIcon(@NonNull Icon icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /** Sets the list of save / get credential entries to be displayed to the user. */
+        @NonNull
+        public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) {
+            mCredentialEntries = credentialEntries;
+            return this;
+        }
+
+        /** Sets the list of action chips to be displayed to the user. */
+        @NonNull
+        public Builder setActionChips(@NonNull List<Entry> actionChips) {
+            mActionChips = actionChips;
+            return this;
+        }
+
+        /** Sets the authentication entry to be displayed to the user. */
+        @NonNull
+        public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) {
+            mAuthenticationEntry = authenticationEntry;
+            return this;
+        }
+
+        /** Sets the time when the provider was last used. */
+        @NonNull
+        public Builder setLastUsedTimeMillis(@CurrentTimeMillisLong long lastUsedTimeMillis) {
+            mLastUsedTimeMillis = lastUsedTimeMillis;
+            return this;
+        }
+
+        /** Builds a {@link ProviderData}. */
+        @NonNull
+        public ProviderData build() {
+            return new ProviderData(mProviderId, mProviderDisplayName, mIcon, mCredentialEntries,
+                mActionChips, mAuthenticationEntry, mLastUsedTimeMillis);
+        }
+    }
 }
diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java
index 5de6d73..eddb519 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -36,12 +36,6 @@
      */
     public static final @NonNull String EXTRA_REQUEST_INFO =
             "android.credentials.ui.extra.REQUEST_INFO";
-    /**
-     * The intent extra key for the {@code ResultReceiver} object when launching the UX
-     * activities.
-     */
-    public static final @NonNull String EXTRA_RESULT_RECEIVER =
-            "android.credentials.ui.extra.RESULT_RECEIVER";
 
     /** Type value for an executeGetCredential request. */
     public static final @NonNull String TYPE_GET = "android.credentials.ui.TYPE_GET";
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index dff2f7e..50551fee 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -133,9 +133,6 @@
     private HandlerThread mHandlerThread;
     private Handler mHandler;
     private FoldStateListener mFoldStateListener;
-    @GuardedBy("mLock")
-    private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = new ArrayList<>();
-    private boolean mFoldedDeviceState;
 
     /**
      * @hide
@@ -144,31 +141,39 @@
         void onDeviceStateChanged(boolean folded);
     }
 
-    private final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+    private static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
         private final int[] mFoldedDeviceStates;
 
+        private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners =
+                new ArrayList<>();
+        private boolean mFoldedDeviceState;
+
         public FoldStateListener(Context context) {
             mFoldedDeviceStates = context.getResources().getIntArray(
                     com.android.internal.R.array.config_foldedDeviceStates);
         }
 
-        private void handleStateChange(int state) {
+        private synchronized void handleStateChange(int state) {
             boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
-            synchronized (mLock) {
-                mFoldedDeviceState = folded;
-                ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
-                for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
-                    DeviceStateListener callback = listener.get();
-                    if (callback != null) {
-                        callback.onDeviceStateChanged(folded);
-                    } else {
-                        invalidListeners.add(listener);
-                    }
-                }
-                if (!invalidListeners.isEmpty()) {
-                    mDeviceStateListeners.removeAll(invalidListeners);
+
+            mFoldedDeviceState = folded;
+            ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
+            for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
+                DeviceStateListener callback = listener.get();
+                if (callback != null) {
+                    callback.onDeviceStateChanged(folded);
+                } else {
+                    invalidListeners.add(listener);
                 }
             }
+            if (!invalidListeners.isEmpty()) {
+                mDeviceStateListeners.removeAll(invalidListeners);
+            }
+        }
+
+        public synchronized void addDeviceStateListener(DeviceStateListener listener) {
+            listener.onDeviceStateChanged(mFoldedDeviceState);
+            mDeviceStateListeners.add(new WeakReference<>(listener));
         }
 
         @Override
@@ -192,9 +197,8 @@
     public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) {
         synchronized (mLock) {
             DeviceStateListener listener = chars.getDeviceStateListener();
-            listener.onDeviceStateChanged(mFoldedDeviceState);
             if (mFoldStateListener != null) {
-                mDeviceStateListeners.add(new WeakReference<>(listener));
+                mFoldStateListener.addDeviceStateListener(listener);
             }
         }
     }
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index 7247ef7..197739b 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -768,6 +768,20 @@
         }
     }
 
+    /**
+     * Schedules a watchdog.
+     *
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void scheduleWatchdog() {
+        try {
+            mService.scheduleWatchdog();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     private void cancelEnrollment(long requestId) {
         if (mService != null) {
             try {
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 9b56f43..2bf187a 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -172,4 +172,9 @@
 
     // Registers BiometricStateListener.
     void registerBiometricStateListener(IBiometricStateListener listener);
+
+    // Internal operation used to clear face biometric scheduler.
+    // Ensures that the scheduler is not stuck.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void scheduleWatchdog();
 }
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 0fd164d..5403f08 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -1080,7 +1080,7 @@
      */
     public boolean isPowerbuttonFps() {
         final FingerprintSensorPropertiesInternal sensorProps = getFirstFingerprintSensor();
-        return sensorProps.sensorType == TYPE_POWER_BUTTON;
+        return sensorProps == null ? false : sensorProps.sensorType == TYPE_POWER_BUTTON;
     }
 
     /**
@@ -1125,6 +1125,20 @@
     }
 
     /**
+     * Schedules a watchdog.
+     *
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void scheduleWatchdog() {
+        try {
+            mService.scheduleWatchdog();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * @hide
      */
     public void addLockoutResetCallback(final LockoutResetCallback callback) {
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index 1ba9a04..051e3a4 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -208,4 +208,9 @@
     // Sends a power button pressed event to all listeners.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     oneway void onPowerPressed();
+
+    // Internal operation used to clear fingerprint biometric scheduler.
+    // Ensures that the scheduler is not stuck.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void scheduleWatchdog();
 }
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index d7cda9e..4d61553 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -24,7 +24,6 @@
 import android.os.RemoteException;
 import android.view.KeyEvent;
 
-import java.io.Closeable;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -39,7 +38,7 @@
  * @hide
  */
 @SystemApi
-public class VirtualDpad implements Closeable {
+public class VirtualDpad extends VirtualInputDevice {
 
     private final Set<Integer> mSupportedKeyCodes =
             Collections.unmodifiableSet(
@@ -50,23 +49,10 @@
                                     KeyEvent.KEYCODE_DPAD_LEFT,
                                     KeyEvent.KEYCODE_DPAD_RIGHT,
                                     KeyEvent.KEYCODE_DPAD_CENTER)));
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
 
     /** @hide */
     public VirtualDpad(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
new file mode 100644
index 0000000..772ba8e
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -0,0 +1,73 @@
+/*
+ * 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.hardware.input;
+
+import android.annotation.RequiresPermission;
+import android.companion.virtual.IVirtualDevice;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.Closeable;
+
+/**
+ * The base class for all virtual input devices such as VirtualKeyboard, VirtualMouse.
+ * This implements the shared functionality such as closing the device and keeping track of
+ * identifiers.
+ *
+ * @hide
+ */
+abstract class VirtualInputDevice implements Closeable {
+
+    /**
+     * The virtual device to which this VirtualInputDevice belongs to.
+     */
+    protected final IVirtualDevice mVirtualDevice;
+
+    /**
+     * The token used to uniquely identify the virtual input device.
+     */
+    protected final IBinder mToken;
+
+    /** @hide */
+    VirtualInputDevice(
+            IVirtualDevice virtualDevice, IBinder token) {
+        mVirtualDevice = virtualDevice;
+        mToken = token;
+    }
+
+    /**
+     * @return The device id of this device.
+     * @hide
+     */
+    public int getInputDeviceId() {
+        try {
+            return mVirtualDevice.getInputDeviceId(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void close() {
+        try {
+            mVirtualDevice.unregisterInputDevice(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
index 901401fe..e569dbf 100644
--- a/core/java/android/hardware/input/VirtualKeyboard.java
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -24,8 +24,6 @@
 import android.os.RemoteException;
 import android.view.KeyEvent;
 
-import java.io.Closeable;
-
 /**
  * A virtual keyboard representing a key input mechanism on a remote device, such as a built-in
  * keyboard on a laptop, a software keyboard on a tablet, or a keypad on a TV remote control.
@@ -36,26 +34,13 @@
  * @hide
  */
 @SystemApi
-public class VirtualKeyboard implements Closeable {
+public class VirtualKeyboard extends VirtualInputDevice {
 
     private final int mUnsupportedKeyCode = KeyEvent.KEYCODE_DPAD_CENTER;
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
 
     /** @hide */
     public VirtualKeyboard(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
index 6e2b56a..7eba2b8 100644
--- a/core/java/android/hardware/input/VirtualMouse.java
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -25,8 +25,6 @@
 import android.os.RemoteException;
 import android.view.MotionEvent;
 
-import java.io.Closeable;
-
 /**
  * A virtual mouse representing a relative input mechanism on a remote device, such as a mouse or
  * trackpad.
@@ -37,25 +35,11 @@
  * @hide
  */
 @SystemApi
-public class VirtualMouse implements Closeable {
-
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
+public class VirtualMouse extends VirtualInputDevice {
 
     /** @hide */
     public VirtualMouse(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
index c8d602a..0d07753 100644
--- a/core/java/android/hardware/input/VirtualTouchscreen.java
+++ b/core/java/android/hardware/input/VirtualTouchscreen.java
@@ -23,8 +23,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 
-import java.io.Closeable;
-
 /**
  * A virtual touchscreen representing a touch-based display input mechanism on a remote device.
  *
@@ -34,25 +32,10 @@
  * @hide
  */
 @SystemApi
-public class VirtualTouchscreen implements Closeable {
-
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
-
+public class VirtualTouchscreen extends VirtualInputDevice {
     /** @hide */
     public VirtualTouchscreen(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java
index 8febed3..3ba3ebc 100644
--- a/core/java/android/hardware/radio/Announcement.java
+++ b/core/java/android/hardware/radio/Announcement.java
@@ -85,9 +85,9 @@
     /** @hide */
     public Announcement(@NonNull ProgramSelector selector, @Type int type,
             @NonNull Map<String, String> vendorInfo) {
-        mSelector = Objects.requireNonNull(selector);
-        mType = Objects.requireNonNull(type);
-        mVendorInfo = Objects.requireNonNull(vendorInfo);
+        mSelector = Objects.requireNonNull(selector, "Program selector cannot be null");
+        mType = type;
+        mVendorInfo = Objects.requireNonNull(vendorInfo, "Vendor info cannot be null");
     }
 
     private Announcement(@NonNull Parcel in) {
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index f2525d1..ade9fd6 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -160,6 +160,7 @@
      * Disables list updates and releases all resources.
      */
     public void close() {
+        OnCloseListener onCompleteListenersCopied = null;
         synchronized (mLock) {
             if (mIsClosed) return;
             mIsClosed = true;
@@ -167,10 +168,14 @@
             mListCallbacks.clear();
             mOnCompleteListeners.clear();
             if (mOnCloseListener != null) {
-                mOnCloseListener.onClose();
+                onCompleteListenersCopied = mOnCloseListener;
                 mOnCloseListener = null;
             }
         }
+
+        if (onCompleteListenersCopied != null) {
+            onCompleteListenersCopied.onClose();
+        }
     }
 
     void apply(Chunk chunk) {
diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
index 891da24..4f09bee 100644
--- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
+++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
@@ -777,7 +777,7 @@
     }
 
     /**
-     * Invokes {@link IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
+     * Invokes {@code IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
      * CharSequence, TextAttribute)}.
      *
      * @param start the character index where the replacement should start.
@@ -788,6 +788,8 @@
      *     that this means you can't position the cursor within the text.
      * @param text the text to replace. This may include styles.
      * @param textAttribute The extra information about the text. This value may be null.
+     * @return {@code true} if the invocation is completed without {@link RemoteException}, {@code
+     *     false} otherwise.
      */
     @AnyThread
     public boolean replaceText(
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 4df0139..d3a6323 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -1246,8 +1246,21 @@
         // If the call was {@link IBinder#FLAG_ONEWAY} then these exceptions
         // disappear into the ether.
         final boolean tagEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_AIDL);
+        final boolean hasFullyQualifiedName = getMaxTransactionId() > 0;
         final String transactionTraceName;
-        if (tagEnabled) {
+
+        if (tagEnabled && hasFullyQualifiedName) {
+            // If tracing enabled and we have a fully qualified name, fetch the name
+            transactionTraceName = getTransactionTraceName(code);
+        } else if (tagEnabled && isStackTrackingEnabled()) {
+            // If tracing is enabled and we *don't* have a fully qualified name, fetch the
+            // 'best effort' name only for stack tracking. This works around noticeable perf impact
+            // on low latency binder calls (<100us). The tracing call itself is between (1-10us) and
+            // the perf impact can be quite noticeable while benchmarking such binder calls.
+            // The primary culprits are ContentProviders and Cursors which convenienty don't
+            // autogenerate their AIDL and hence will not have a fully qualified name.
+            //
+            // TODO(b/253426478): Relax this constraint after a more robust fix
             transactionTraceName = getTransactionTraceName(code);
         } else {
             transactionTraceName = null;
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index f62cc87..8afd6de 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -341,4 +341,10 @@
      * device is not awake.
      */
     public abstract void nap(long eventTime, boolean allowWake);
+
+    /**
+     * Returns true if ambient display is suppressed by any app with any token. This method will
+     * return false if ambient display is not available.
+     */
+    public abstract boolean isAmbientDisplaySuppressed();
 }
diff --git a/core/java/android/os/storage/OWNERS b/core/java/android/os/storage/OWNERS
index 1f686e5..c80c57c 100644
--- a/core/java/android/os/storage/OWNERS
+++ b/core/java/android/os/storage/OWNERS
@@ -1,11 +1,15 @@
 # Bug component: 95221
 
-corinac@google.com
-nandana@google.com
-zezeozue@google.com
-maco@google.com
-sahanas@google.com
+# Android Storage Team
 abkaur@google.com
-chiangi@google.com
-narayan@google.com
+corinac@google.com
 dipankarb@google.com
+krishang@google.com
+sahanas@google.com
+sergeynv@google.com
+shubhisaxena@google.com
+tylersaunders@google.com
+
+maco@google.com
+nandana@google.com
+narayan@google.com
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4e15b38..29e2459 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6875,6 +6875,14 @@
         @Readable
         public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service";
 
+
+        /**
+         * The currently selected credential service(s) flattened ComponentName.
+         *
+         * @hide
+         */
+        public static final String CREDENTIAL_SERVICE = "credential_service";
+
         /**
          * The currently selected autofill service flattened ComponentName.
          * @hide
diff --git a/core/java/android/service/credentials/Action.java b/core/java/android/service/credentials/Action.java
index e2c11fb..553a324 100644
--- a/core/java/android/service/credentials/Action.java
+++ b/core/java/android/service/credentials/Action.java
@@ -50,9 +50,8 @@
     }
 
     private Action(@NonNull Parcel in) {
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
     }
 
     public static final @NonNull Creator<Action> CREATOR = new Creator<Action>() {
@@ -74,8 +73,8 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
     }
 
     /**
diff --git a/core/java/android/service/credentials/CreateCredentialRequest.java b/core/java/android/service/credentials/CreateCredentialRequest.java
index 6a0bbc0..e6da349 100644
--- a/core/java/android/service/credentials/CreateCredentialRequest.java
+++ b/core/java/android/service/credentials/CreateCredentialRequest.java
@@ -54,7 +54,7 @@
     private CreateCredentialRequest(@NonNull Parcel in) {
         mCallingPackage = in.readString8();
         mType = in.readString8();
-        mData = in.readBundle();
+        mData = in.readTypedObject(Bundle.CREATOR);
     }
 
     public static final @NonNull Creator<CreateCredentialRequest> CREATOR =
@@ -79,7 +79,7 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mCallingPackage);
         dest.writeString8(mType);
-        dest.writeBundle(mData);
+        dest.writeTypedObject(mData, flags);
     }
 
     /** Returns the calling package of the calling app. */
diff --git a/core/java/android/service/credentials/CreateCredentialResponse.java b/core/java/android/service/credentials/CreateCredentialResponse.java
index 613eba8..559b1ca 100644
--- a/core/java/android/service/credentials/CreateCredentialResponse.java
+++ b/core/java/android/service/credentials/CreateCredentialResponse.java
@@ -38,7 +38,9 @@
 
     private CreateCredentialResponse(@NonNull Parcel in) {
         mHeader = in.readCharSequence();
-        mSaveEntries = in.createTypedArrayList(SaveEntry.CREATOR);
+        List<SaveEntry> saveEntries = new ArrayList<>();
+        in.readTypedList(saveEntries, SaveEntry.CREATOR);
+        mSaveEntries = saveEntries;
     }
 
     @Override
diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java
index 49b8435..4cc43a1 100644
--- a/core/java/android/service/credentials/CredentialEntry.java
+++ b/core/java/android/service/credentials/CredentialEntry.java
@@ -65,12 +65,10 @@
     }
 
     private CredentialEntry(@NonNull Parcel in) {
-        mType = in.readString();
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
-        mCredential = in.readParcelable(Credential.class.getClassLoader(),
-                Credential.class);
+        mType = in.readString8();
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
+        mCredential = in.readTypedObject(Credential.CREATOR);
         mAutoSelectAllowed = in.readBoolean();
     }
 
@@ -95,9 +93,9 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mType);
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
-        mCredential.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
+        dest.writeTypedObject(mCredential, flags);
         dest.writeBoolean(mAutoSelectAllowed);
     }
 
diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java
new file mode 100644
index 0000000..e3f8cb7
--- /dev/null
+++ b/core/java/android/service/credentials/CredentialProviderInfo.java
@@ -0,0 +1,184 @@
+/*
+ * 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.service.credentials;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link ServiceInfo} and meta-data about a credential provider.
+ *
+ * @hide
+ */
+public final class CredentialProviderInfo {
+    private static final String TAG = "CredentialProviderInfo";
+
+    @NonNull
+    private final ServiceInfo mServiceInfo;
+    @NonNull
+    private final List<String> mCapabilities;
+
+    // TODO: Move the two strings below to CredentialProviderService when ready.
+    private static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";
+    private static final String SERVICE_INTERFACE =
+            "android.service.credentials.CredentialProviderService";
+
+
+    /**
+     * Constructs an information instance of the credential provider.
+     *
+     * @param context The context object
+     * @param serviceComponent The serviceComponent of the provider service
+     * @param userId The android userId for which the current process is running
+     * @throws PackageManager.NameNotFoundException If provider service is not found
+     */
+    public CredentialProviderInfo(@NonNull Context context,
+            @NonNull ComponentName serviceComponent, int userId)
+            throws PackageManager.NameNotFoundException {
+        this(context, getServiceInfoOrThrow(serviceComponent, userId));
+    }
+
+    private CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) {
+        if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) {
+            Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName
+                    + "does not require permission"
+                    + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE);
+            throw new SecurityException("Service does not require the expected permission : "
+                    + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE);
+        }
+        mServiceInfo = serviceInfo;
+        mCapabilities = new ArrayList<>();
+        populateProviderCapabilities(context);
+    }
+
+    private void populateProviderCapabilities(@NonNull Context context) {
+        if (mServiceInfo.applicationInfo.metaData == null) {
+            return;
+        }
+        try {
+            final int resourceId = mServiceInfo.applicationInfo.metaData.getInt(
+                    CAPABILITY_META_DATA_KEY);
+            String[] capabilities = context.getResources().getStringArray(resourceId);
+            if (capabilities == null) {
+                Log.w(TAG, "No capabilities found for provider: " + mServiceInfo.packageName);
+                return;
+            }
+            for (String capability : capabilities) {
+                if (capability.isEmpty()) {
+                    Log.w(TAG, "Skipping empty capability");
+                    continue;
+                }
+                mCapabilities.add(capability);
+            }
+        } catch (Resources.NotFoundException e) {
+            Log.w(TAG, "Exception while populating provider capabilities: " + e.getMessage());
+        }
+    }
+
+    private static ServiceInfo getServiceInfoOrThrow(@NonNull ComponentName serviceComponent,
+            int userId) throws PackageManager.NameNotFoundException {
+        try {
+            ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo(
+                    serviceComponent,
+                    PackageManager.GET_META_DATA,
+                    userId);
+            if (si != null) {
+                return si;
+            }
+        } catch (RemoteException e) {
+            Slog.v(TAG, e.getMessage());
+        }
+        throw new PackageManager.NameNotFoundException(serviceComponent.toString());
+    }
+
+    /**
+     * Returns true if the service supports the given {@code credentialType}, false otherwise.
+     */
+    @NonNull
+    public boolean hasCapability(@NonNull String credentialType) {
+        return mCapabilities.contains(credentialType);
+    }
+
+    /** Returns the service info. */
+    @NonNull
+    public ServiceInfo getServiceInfo() {
+        return mServiceInfo;
+    }
+
+    /** Returns an immutable list of capabilities this provider service can support. */
+    @NonNull
+    public List<String> getCapabilities() {
+        return Collections.unmodifiableList(mCapabilities);
+    }
+
+    /**
+     * Returns the valid credential provider services available for the user with the
+     * given {@code userId}.
+     */
+    public static List<CredentialProviderInfo> getAvailableServices(@NonNull Context context,
+            @UserIdInt int userId) {
+        final List<CredentialProviderInfo> services = new ArrayList<>();
+
+        final List<ResolveInfo> resolveInfos =
+                context.getPackageManager().queryIntentServicesAsUser(
+                        new Intent(SERVICE_INTERFACE),
+                        PackageManager.GET_META_DATA,
+                        userId);
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+            try {
+                services.add(new CredentialProviderInfo(context, serviceInfo));
+            } catch (SecurityException e) {
+                Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e);
+            }
+        }
+        return services;
+    }
+
+    /**
+     * Returns the valid credential provider services available for the user, that can
+     * support the given {@code credentialType}.
+     */
+    public static List<CredentialProviderInfo> getAvailableServicesForCapability(
+            Context context, @UserIdInt int userId, String credentialType) {
+        List<CredentialProviderInfo> servicesForCapability = new ArrayList<>();
+        final List<CredentialProviderInfo> services = getAvailableServices(context, userId);
+
+        for (CredentialProviderInfo service : services) {
+            if (service.hasCapability(credentialType)) {
+                servicesForCapability.add(service);
+            }
+        }
+        return servicesForCapability;
+    }
+}
diff --git a/core/java/android/service/credentials/CredentialsDisplayContent.java b/core/java/android/service/credentials/CredentialsDisplayContent.java
index 4133ea5..2cce169 100644
--- a/core/java/android/service/credentials/CredentialsDisplayContent.java
+++ b/core/java/android/service/credentials/CredentialsDisplayContent.java
@@ -53,8 +53,12 @@
 
     private CredentialsDisplayContent(@NonNull Parcel in) {
         mHeader = in.readCharSequence();
-        mCredentialEntries = in.createTypedArrayList(CredentialEntry.CREATOR);
-        mActions = in.createTypedArrayList(Action.CREATOR);
+        List<CredentialEntry> credentialEntries = new ArrayList<>();
+        in.readTypedList(credentialEntries, CredentialEntry.CREATOR);
+        mCredentialEntries = credentialEntries;
+        List<Action> actions = new ArrayList<>();
+        in.readTypedList(actions, Action.CREATOR);
+        mActions = actions;
     }
 
     public static final @NonNull Creator<CredentialsDisplayContent> CREATOR =
@@ -78,8 +82,8 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeCharSequence(mHeader);
-        dest.writeTypedList(mCredentialEntries);
-        dest.writeTypedList(mActions);
+        dest.writeTypedList(mCredentialEntries, flags);
+        dest.writeTypedList(mActions, flags);
     }
 
     /**
diff --git a/core/java/android/service/credentials/GetCredentialsRequest.java b/core/java/android/service/credentials/GetCredentialsRequest.java
index 5b1a171..e06be44 100644
--- a/core/java/android/service/credentials/GetCredentialsRequest.java
+++ b/core/java/android/service/credentials/GetCredentialsRequest.java
@@ -49,8 +49,10 @@
     }
 
     private GetCredentialsRequest(@NonNull Parcel in) {
-        mCallingPackage = in.readString16NoHelper();
-        mGetCredentialOptions = in.createTypedArrayList(GetCredentialOption.CREATOR);
+        mCallingPackage = in.readString8();
+        List<GetCredentialOption> getCredentialOptions = new ArrayList<>();
+        in.readTypedList(getCredentialOptions, GetCredentialOption.CREATOR);
+        mGetCredentialOptions = getCredentialOptions;
     }
 
     public static final @NonNull Creator<GetCredentialsRequest> CREATOR =
@@ -73,7 +75,7 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeString16NoHelper(mCallingPackage);
+        dest.writeString8(mCallingPackage);
         dest.writeTypedList(mGetCredentialOptions);
     }
 
diff --git a/core/java/android/service/credentials/GetCredentialsResponse.java b/core/java/android/service/credentials/GetCredentialsResponse.java
index 980d9ae..979a699 100644
--- a/core/java/android/service/credentials/GetCredentialsResponse.java
+++ b/core/java/android/service/credentials/GetCredentialsResponse.java
@@ -78,9 +78,8 @@
     }
 
     private GetCredentialsResponse(@NonNull Parcel in) {
-        mCredentialsDisplayContent = in.readParcelable(CredentialsDisplayContent.class
-                .getClassLoader(), CredentialsDisplayContent.class);
-        mAuthenticationAction = in.readParcelable(Action.class.getClassLoader(), Action.class);
+        mCredentialsDisplayContent = in.readTypedObject(CredentialsDisplayContent.CREATOR);
+        mAuthenticationAction = in.readTypedObject(Action.CREATOR);
     }
 
     public static final @NonNull Creator<GetCredentialsResponse> CREATOR =
@@ -103,8 +102,8 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeParcelable(mCredentialsDisplayContent, flags);
-        dest.writeParcelable(mAuthenticationAction, flags);
+        dest.writeTypedObject(mCredentialsDisplayContent, flags);
+        dest.writeTypedObject(mAuthenticationAction, flags);
     }
 
     /**
diff --git a/core/java/android/service/credentials/SaveEntry.java b/core/java/android/service/credentials/SaveEntry.java
index 18644f0..abe51d4 100644
--- a/core/java/android/service/credentials/SaveEntry.java
+++ b/core/java/android/service/credentials/SaveEntry.java
@@ -40,10 +40,9 @@
     private final @Nullable Credential mCredential;
 
     private SaveEntry(@NonNull Parcel in) {
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
-        mCredential = in.readParcelable(Credential.class.getClassLoader(), Credential.class);
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
+        mCredential = in.readTypedObject(Credential.CREATOR);
     }
 
     public static final @NonNull Creator<SaveEntry> CREATOR = new Creator<SaveEntry>() {
@@ -65,9 +64,9 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
-        mCredential.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
+        dest.writeTypedObject(mCredential, flags);
     }
 
     /* package-private */ SaveEntry(
diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java
index f6a7c8e..a2fa139 100644
--- a/core/java/android/service/dreams/DreamActivity.java
+++ b/core/java/android/service/dreams/DreamActivity.java
@@ -44,6 +44,8 @@
 public class DreamActivity extends Activity {
     static final String EXTRA_CALLBACK = "binder";
     static final String EXTRA_DREAM_TITLE = "title";
+    @Nullable
+    private DreamService.DreamActivityCallbacks mCallback;
 
     public DreamActivity() {}
 
@@ -57,11 +59,19 @@
         }
 
         final Bundle extras = getIntent().getExtras();
-        final DreamService.DreamActivityCallback callback =
-                (DreamService.DreamActivityCallback) extras.getBinder(EXTRA_CALLBACK);
+        mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK);
 
-        if (callback != null) {
-            callback.onActivityCreated(this);
+        if (mCallback != null) {
+            mCallback.onActivityCreated(this);
         }
     }
+
+    @Override
+    public void onDestroy() {
+        if (mCallback != null) {
+            mCallback.onActivityDestroyed();
+        }
+
+        super.onDestroy();
+    }
 }
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 3c1fef0..32bdf79 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -1047,7 +1047,7 @@
         }
 
         if (mDreamToken == null) {
-            Slog.w(mTag, "Finish was called before the dream was attached.");
+            if (mDebug) Slog.v(mTag, "finish() called when not attached.");
             stopSelf();
             return;
         }
@@ -1295,7 +1295,7 @@
             Intent i = new Intent(this, DreamActivity.class);
             i.setPackage(getApplicationContext().getPackageName());
             i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallback(mDreamToken));
+            i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken));
             final ServiceInfo serviceInfo = fetchServiceInfo(this,
                     new ComponentName(this, getClass()));
             i.putExtra(DreamActivity.EXTRA_DREAM_TITLE, fetchDreamLabel(this, serviceInfo));
@@ -1488,10 +1488,10 @@
     }
 
     /** @hide */
-    final class DreamActivityCallback extends Binder {
+    final class DreamActivityCallbacks extends Binder {
         private final IBinder mActivityDreamToken;
 
-        DreamActivityCallback(IBinder token) {
+        DreamActivityCallbacks(IBinder token) {
             mActivityDreamToken = token;
         }
 
@@ -1516,6 +1516,12 @@
             mActivity = activity;
             onWindowCreated(activity.getWindow());
         }
+
+        // If DreamActivity is destroyed, wake up from Dream.
+        void onActivityDestroyed() {
+            mActivity = null;
+            onDestroy();
+        }
     }
 
     /**
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index bd4a495..cfc79e4 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -392,13 +392,13 @@
     public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3;
 
     /**
-     * An optional activity intent category that shows additional settings for what notifications
+     * An optional activity intent action that shows additional settings for what notifications
      * should be processed by this notification listener service. If defined, the OS may link to
      * this activity from the system notification listener service filter settings page.
      */
-    @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY)
-    public static final String INTENT_CATEGORY_SETTINGS_HOME =
-            "android.service.notification.category.SETTINGS_HOME";
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_SETTINGS_HOME =
+            "android.service.notification.action.SETTINGS_HOME";
 
     private final Object mLock = new Object();
 
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index b559161..a59d429 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -284,7 +284,6 @@
         private Display mDisplay;
         private Context mDisplayContext;
         private int mDisplayState;
-        private @Surface.Rotation int mDisplayInstallOrientation;
         private float mWallpaperDimAmount = 0.05f;
         private float mPreviousWallpaperDimAmount = mWallpaperDimAmount;
         private float mDefaultDimAmount = mWallpaperDimAmount;
@@ -1159,7 +1158,7 @@
                             mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle);
 
                     final int transformHint = SurfaceControl.rotationToBufferTransform(
-                            (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
+                            (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4);
                     mSurfaceControl.setTransformHint(transformHint);
                     WindowLayout.computeSurfaceSize(mLayout, maxBounds, mWidth, mHeight,
                             mWinFrames.frame, false /* dragResizing */, mSurfaceSize);
@@ -1420,7 +1419,6 @@
             mWallpaperDimAmount = mDefaultDimAmount;
             mPreviousWallpaperDimAmount = mWallpaperDimAmount;
             mDisplayState = mDisplay.getState();
-            mDisplayInstallOrientation = mDisplay.getInstallOrientation();
 
             if (DEBUG) Log.v(TAG, "onCreate(): " + this);
             onCreate(mSurfaceHolder);
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 519fc55..1337d6a 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -36,7 +36,6 @@
 import android.text.style.ParagraphStyle;
 import android.text.style.ReplacementSpan;
 import android.text.style.TabStopSpan;
-import android.util.Range;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -1859,13 +1858,12 @@
      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
      *     text segment
      * @param inclusionStrategy strategy for determining whether a text segment is inside the
-     *          specified area
-     * @return an integer range where the endpoints are the start (inclusive) and end (exclusive)
-     *     character offsets of the text range, or null if there are no text segments inside the
-     *     area
+     *     specified area
+     * @return int array of size 2 containing the start (inclusive) and end (exclusive) character
+     *     offsets of the text range, or null if there are no text segments inside the area
      */
     @Nullable
-    public Range<Integer> getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
+    public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
             @NonNull TextInclusionStrategy inclusionStrategy) {
         // Find the first line whose bottom (without line spacing) is below the top of the area.
         int startLine = getLineForVertical((int) area.top);
@@ -1923,7 +1921,7 @@
         start = segmentFinder.previousStartBoundary(start + 1);
         end = segmentFinder.nextEndBoundary(end - 1);
 
-        return new Range(start, end);
+        return new int[] {start, end};
     }
 
     /**
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index a1ece92..a0a07b3 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -162,7 +162,13 @@
                         if (candidateView == getConnectedView()) {
                             startHandwriting(candidateView);
                         } else {
-                            candidateView.requestFocus();
+                            if (candidateView.getRevealOnFocusHint()) {
+                                candidateView.setRevealOnFocusHint(false);
+                                candidateView.requestFocus();
+                                candidateView.setRevealOnFocusHint(true);
+                            } else {
+                                candidateView.requestFocus();
+                            }
                         }
                     }
                 }
diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java
index 4de7c4f..43828d5 100644
--- a/core/java/android/view/ImeFocusController.java
+++ b/core/java/android/view/ImeFocusController.java
@@ -108,10 +108,11 @@
     }
 
     /**
-     * @see InputMethodManager#checkFocus()
+     * @see ViewRootImpl#dispatchCheckFocus()
      */
-    public boolean checkFocus(boolean forceNewFocus, boolean startInput) {
-        return getImmDelegate().checkFocus(forceNewFocus, startInput, mViewRootImpl);
+    @UiThread
+    void onScheduledCheckFocus() {
+        getImmDelegate().onScheduledCheckFocus(mViewRootImpl);
     }
 
     @UiThread
@@ -163,7 +164,7 @@
         void onPostWindowGainedFocus(View viewForWindowFocus,
                 @NonNull WindowManager.LayoutParams windowAttribute);
         void onViewFocusChanged(@NonNull View view, boolean hasFocus);
-        boolean checkFocus(boolean forceNewFocus, boolean startInput, ViewRootImpl viewRootImpl);
+        void onScheduledCheckFocus(@NonNull ViewRootImpl viewRootImpl);
         void onViewDetachedFromWindow(View view, ViewRootImpl viewRootImpl);
         void onWindowDismissed(ViewRootImpl viewRootImpl);
     }
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index 9789b56..06c1b25 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -2001,7 +2001,6 @@
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 58c8126..efda257 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -428,8 +428,6 @@
     final DisplayManager mDisplayManager;
     final String mBasePackageName;
 
-    private @Surface.Rotation int mDisplayInstallOrientation;
-
     final int[] mTmpLocation = new int[2];
 
     final TypedValue mTmpValue = new TypedValue();
@@ -890,20 +888,18 @@
     static BLASTBufferQueue.TransactionHangCallback sTransactionHangCallback =
         new BLASTBufferQueue.TransactionHangCallback() {
             @Override
-            public void onTransactionHang(boolean isGPUHang) {
-                if (isGPUHang && !sAnrReported) {
-                    sAnrReported = true;
-                    try {
-                        ActivityManager.getService().appNotResponding(
-                            "Buffer processing hung up due to stuck fence. Indicates GPU hang");
-                    } catch (RemoteException e) {
-                        // We asked the system to crash us, but the system
-                        // already crashed. Unfortunately things may be
-                        // out of control.
-                    }
-                } else {
-                    // TODO: Do something with this later. For now we just ANR
-                    // in dequeue buffer later like we always have.
+            public void onTransactionHang(String reason) {
+                if (sAnrReported) {
+                    return;
+                }
+
+                sAnrReported = true;
+                try {
+                    ActivityManager.getService().appNotResponding(reason);
+                } catch (RemoteException e) {
+                    // We asked the system to crash us, but the system
+                    // already crashed. Unfortunately things may be
+                    // out of control.
                 }
             }
         };
@@ -1134,7 +1130,6 @@
             if (mView == null) {
                 mView = view;
 
-                mDisplayInstallOrientation = mDisplay.getInstallOrientation();
                 mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                 mFallbackEventHandler.setView(view);
                 mWindowAttributes.copyFrom(attrs);
@@ -1905,7 +1900,6 @@
         updateInternalDisplay(displayId, mView.getResources());
         mImeFocusController.onMovedToDisplay();
         mAttachInfo.mDisplayState = mDisplay.getState();
-        mDisplayInstallOrientation = mDisplay.getInstallOrientation();
         // Internal state updated, now notify the view hierarchy.
         mView.dispatchMovedToDisplay(mDisplay, config);
     }
@@ -5718,7 +5712,7 @@
                     enqueueInputEvent(event, null, 0, true);
                 } break;
                 case MSG_CHECK_FOCUS: {
-                    getImeFocusController().checkFocus(false, true);
+                    getImeFocusController().onScheduledCheckFocus();
                 } break;
                 case MSG_CLOSE_SYSTEM_DIALOGS: {
                     if (mView != null) {
@@ -8235,7 +8229,7 @@
         }
 
         final int transformHint = SurfaceControl.rotationToBufferTransform(
-                (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
+                (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4);
 
         WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth,
                 requestedHeight, mWinFrameInScreen, mPendingDragResizing, mSurfaceSize);
@@ -8260,7 +8254,7 @@
         }
 
         mLastTransformHint = transformHint;
-      
+
         mSurfaceControl.setTransformHint(transformHint);
 
         if (mAttachInfo.mContentCaptureManager != null) {
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index 4a79ba6..febdac2 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -595,6 +595,10 @@
                 == HandwritingGesture.GESTURE_TYPE_SELECT) {
             list.add(SelectGesture.class);
         }
+        if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_SELECT_RANGE)
+                == HandwritingGesture.GESTURE_TYPE_SELECT_RANGE) {
+            list.add(SelectRangeGesture.class);
+        }
         if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_INSERT)
                 == HandwritingGesture.GESTURE_TYPE_INSERT) {
             list.add(InsertGesture.class);
@@ -603,6 +607,10 @@
                 == HandwritingGesture.GESTURE_TYPE_DELETE) {
             list.add(DeleteGesture.class);
         }
+        if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_DELETE_RANGE)
+                == HandwritingGesture.GESTURE_TYPE_DELETE_RANGE) {
+            list.add(DeleteRangeGesture.class);
+        }
         if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE)
                 == HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE) {
             list.add(RemoveSpaceGesture.class);
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 18b3e21..975a786 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -30,7 +30,9 @@
 import static android.view.inputmethod.InputMethodManagerProto.ACTIVE;
 import static android.view.inputmethod.InputMethodManagerProto.CUR_ID;
 import static android.view.inputmethod.InputMethodManagerProto.FULLSCREEN_MODE;
+import static android.view.inputmethod.InputMethodManagerProto.NEXT_SERVED_VIEW;
 import static android.view.inputmethod.InputMethodManagerProto.SERVED_CONNECTING;
+import static android.view.inputmethod.InputMethodManagerProto.SERVED_VIEW;
 
 import static com.android.internal.inputmethod.StartInputReason.BOUND_TO_IMMS;
 
@@ -763,39 +765,37 @@
                     forceFocus = true;
                 }
             }
-            startInputOnWindowFocusGain(viewForWindowFocus,
-                    windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
-        }
 
-        private void startInputOnWindowFocusGain(View focusedView,
-                @SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) {
-            int startInputFlags = getStartInputFlags(focusedView, 0);
+            final int softInputMode = windowAttribute.softInputMode;
+            final int windowFlags = windowAttribute.flags;
+
+            int startInputFlags = getStartInputFlags(viewForWindowFocus, 0);
             startInputFlags |= StartInputFlags.WINDOW_GAINED_FOCUS;
 
             ImeTracing.getInstance().triggerClientDump(
                     "InputMethodManager.DelegateImpl#startInputAsyncOnWindowFocusGain",
                     InputMethodManager.this, null /* icProto */);
 
-            final ImeFocusController controller = getFocusController();
-            if (controller == null) {
-                return;
-            }
-
+            boolean checkFocusResult;
             synchronized (mH) {
+                if (mCurRootView == null) {
+                    return;
+                }
                 if (mRestartOnNextWindowFocus) {
                     if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus as true");
                     mRestartOnNextWindowFocus = false;
-                    forceNewFocus = true;
+                    forceFocus = true;
                 }
+                checkFocusResult = checkFocusInternalLocked(forceFocus, mCurRootView);
             }
 
-            if (controller.checkFocus(forceNewFocus, false)) {
+            if (checkFocusResult) {
                 // We need to restart input on the current focus view.  This
                 // should be done in conjunction with telling the system service
                 // about the window gaining focus, to help make the transition
                 // smooth.
                 if (startInputOnWindowFocusGainInternal(StartInputReason.WINDOW_FOCUS_GAIN,
-                        focusedView, startInputFlags, softInputMode, windowFlags)) {
+                        viewForWindowFocus, startInputFlags, softInputMode, windowFlags)) {
                     return;
                 }
             }
@@ -810,7 +810,7 @@
                 // ignore the result
                 mServiceInvoker.startInputOrWindowGainedFocus(
                         StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient,
-                        focusedView.getWindowToken(), startInputFlags, softInputMode,
+                        viewForWindowFocus.getWindowToken(), startInputFlags, softInputMode,
                         windowFlags,
                         null,
                         null, null,
@@ -825,9 +825,15 @@
         }
 
         @Override
-        public boolean checkFocus(boolean forceNewFocus, boolean startInput,
-                ViewRootImpl viewRootImpl) {
-            return checkFocusInternal(forceNewFocus, startInput, viewRootImpl);
+        public void onScheduledCheckFocus(ViewRootImpl viewRootImpl) {
+            synchronized (mH) {
+                if (!checkFocusInternalLocked(false, viewRootImpl)) {
+                    return;
+                }
+            }
+            startInputOnWindowFocusGainInternal(StartInputReason.SCHEDULED_CHECK_FOCUS,
+                    null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */,
+                    0 /* windowFlags */);
         }
 
         @Override
@@ -897,8 +903,6 @@
     /**
      * Checks whether the active input connection (if any) is for the given view.
      *
-     * TODO(b/182259171): Clean-up hasActiveConnection to simplify the logic.
-     *
      * Note that this method is only intended for restarting input after focus gain
      * (e.g. b/160391516), DO NOT leverage this method to do another check.
      */
@@ -909,7 +913,6 @@
             }
 
             return mServedInputConnection != null
-                    && mServedInputConnection.isActive()
                     && mServedInputConnection.isAssociatedWith(view);
         }
     }
@@ -937,15 +940,6 @@
         return mCurRootView != null ? mNextServedView : null;
     }
 
-    private ImeFocusController getFocusController() {
-        synchronized (mH) {
-            if (mCurRootView != null) {
-                return mCurRootView.getImeFocusController();
-            }
-            return null;
-        }
-    }
-
     /**
      * Returns {@code true} when the given view has been served by Input Method.
      */
@@ -1128,8 +1122,7 @@
                         if (mCurRootView == null) {
                             return;
                         }
-                        if (!mCurRootView.getImeFocusController().checkFocus(
-                                mRestartOnNextWindowFocus, false)) {
+                        if (!checkFocusInternalLocked(mRestartOnNextWindowFocus, mCurRootView)) {
                             return;
                         }
                         final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS
@@ -2349,8 +2342,7 @@
     }
 
     /**
-     * Called from {@link #checkFocusInternal(boolean, boolean, ViewRootImpl)},
-     * {@link #restartInput(View)}, {@link #MSG_BIND} or {@link #MSG_UNBIND}.
+     * Starts an input connection from the served view that gains the window focus.
      * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
      * background thread may blocked by other methods which already inside {@code mH} lock.
      */
@@ -2658,52 +2650,53 @@
     }
 
     /**
-     * Check the next served view from {@link ImeFocusController} if needs to start input.
      * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
      * background thread may blocked by other methods which already inside {@code mH} lock.
      * @hide
      */
     @UnsupportedAppUsage
     public void checkFocus() {
-        final ImeFocusController controller = getFocusController();
-        if (controller != null) {
-            controller.checkFocus(false /* forceNewFocus */, true /* startInput */);
+        synchronized (mH) {
+            if (mCurRootView == null) {
+                return;
+            }
+            if (!checkFocusInternalLocked(false /* forceNewFocus */, mCurRootView)) {
+                return;
+            }
         }
+        startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
+                null /* focusedView */,
+                0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
     }
 
-    private boolean checkFocusInternal(boolean forceNewFocus, boolean startInput,
-            ViewRootImpl viewRootImpl) {
-        synchronized (mH) {
-            if (mCurRootView != viewRootImpl) {
-                return false;
-            }
-            if (mServedView == mNextServedView && !forceNewFocus) {
-                return false;
-            }
-            if (DEBUG) {
-                Log.v(TAG, "checkFocus: view=" + mServedView
-                        + " next=" + mNextServedView
-                        + " force=" + forceNewFocus
-                        + " package="
-                        + (mServedView != null ? mServedView.getContext().getPackageName()
-                        : "<none>"));
-            }
-            // Close the connection when no next served view coming.
-            if (mNextServedView == null) {
-                finishInputLocked();
-                closeCurrentInput();
-                return false;
-            }
-            mServedView = mNextServedView;
-            if (mServedInputConnection != null) {
-                mServedInputConnection.finishComposingTextFromImm();
-            }
+    /**
+     * Check the next served view if needs to start input.
+     */
+    @GuardedBy("mH")
+    private boolean checkFocusInternalLocked(boolean forceNewFocus, ViewRootImpl viewRootImpl) {
+        if (mCurRootView != viewRootImpl) {
+            return false;
         }
-
-        if (startInput) {
-            startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
-                    null /* focusedView */,
-                    0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
+        if (mServedView == mNextServedView && !forceNewFocus) {
+            return false;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "checkFocus: view=" + mServedView
+                    + " next=" + mNextServedView
+                    + " force=" + forceNewFocus
+                    + " package="
+                    + (mServedView != null ? mServedView.getContext().getPackageName()
+                    : "<none>"));
+        }
+        // Close the connection when no next served view coming.
+        if (mNextServedView == null) {
+            finishInputLocked();
+            closeCurrentInput();
+            return false;
+        }
+        mServedView = mNextServedView;
+        if (mServedInputConnection != null) {
+            mServedInputConnection.finishComposingTextFromImm();
         }
         return true;
     }
@@ -3996,6 +3989,8 @@
             proto.write(FULLSCREEN_MODE, mFullscreenMode);
             proto.write(ACTIVE, mActive);
             proto.write(SERVED_CONNECTING, mServedConnecting);
+            proto.write(SERVED_VIEW, Objects.toString(mServedView));
+            proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView));
             proto.end(token);
             if (mCurRootView != null) {
                 mCurRootView.dumpDebug(proto, VIEW_ROOT_IMPL);
diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
index fa18eec..f2b7099 100644
--- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
@@ -214,7 +214,7 @@
         }
     }
 
-    public boolean isActive() {
+    private boolean isActive() {
         return mParentInputMethodManager.isActive() && !isFinished();
     }
 
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index a2e9faa..b5c58fb 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -151,7 +151,6 @@
 import android.util.FeatureFlagUtils;
 import android.util.IntArray;
 import android.util.Log;
-import android.util.Range;
 import android.util.SparseIntArray;
 import android.util.TypedValue;
 import android.view.AccessibilityIterators.TextSegmentIterator;
@@ -189,6 +188,7 @@
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.DeleteGesture;
+import android.view.inputmethod.DeleteRangeGesture;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
@@ -199,6 +199,7 @@
 import android.view.inputmethod.JoinOrSplitGesture;
 import android.view.inputmethod.RemoveSpaceGesture;
 import android.view.inputmethod.SelectGesture;
+import android.view.inputmethod.SelectRangeGesture;
 import android.view.inspector.InspectableProperty;
 import android.view.inspector.InspectableProperty.EnumEntry;
 import android.view.inspector.InspectableProperty.FlagEntry;
@@ -9096,7 +9097,9 @@
 
                 ArrayList<Class<? extends HandwritingGesture>> gestures = new ArrayList<>();
                 gestures.add(SelectGesture.class);
+                gestures.add(SelectRangeGesture.class);
                 gestures.add(DeleteGesture.class);
+                gestures.add(DeleteRangeGesture.class);
                 gestures.add(InsertGesture.class);
                 gestures.add(RemoveSpaceGesture.class);
                 gestures.add(JoinOrSplitGesture.class);
@@ -9313,82 +9316,145 @@
 
     /** @hide */
     public int performHandwritingSelectGesture(@NonNull SelectGesture gesture) {
-        Range<Integer> range = getRangeForRect(
+        int[] range = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getSelectionArea()),
                 gesture.getGranularity());
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        Selection.setSelection(getEditableText(), range.getLower(), range.getUpper());
+        Selection.setSelection(getEditableText(), range[0], range[1]);
+        mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
+        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+    }
+
+    /** @hide */
+    public int performHandwritingSelectRangeGesture(@NonNull SelectRangeGesture gesture) {
+        int[] startRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getSelectionStartArea()),
+                gesture.getGranularity());
+        if (startRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        int[] endRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getSelectionEndArea()),
+                gesture.getGranularity());
+        if (endRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        int[] range = new int[] {
+                Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1])
+        };
+        Selection.setSelection(getEditableText(), range[0], range[1]);
         mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
     public int performHandwritingDeleteGesture(@NonNull DeleteGesture gesture) {
-        Range<Integer> range = getRangeForRect(
+        int[] range = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getDeletionArea()),
                 gesture.getGranularity());
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        int start = range.getLower();
-        int end = range.getUpper();
 
-        // For word granularity, adjust the start and end offsets to remove extra whitespace around
-        // the deleted text.
         if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
-            // If the deleted text is at the start of the text, the behavior is the same as the case
-            // where the deleted text follows a new line character.
-            int codePointBeforeStart = start > 0
-                    ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT;
-            // If the deleted text is at the end of the text, the behavior is the same as the case
-            // where the deleted text precedes a new line character.
-            int codePointAtEnd = end < mText.length()
-                    ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT;
-            if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)
-                    && (TextUtils.isWhitespace(codePointAtEnd)
-                            || TextUtils.isPunctuation(codePointAtEnd))) {
-                // Remove whitespace (except new lines) before the deleted text, in these cases:
-                // - There is whitespace following the deleted text
-                //     e.g. "one [deleted] three" -> "one | three" -> "one| three"
-                // - There is punctuation following the deleted text
-                //     e.g. "one [deleted]!" -> "one |!" -> "one|!"
-                // - There is a new line following the deleted text
-                //     e.g. "one [deleted]\n" -> "one |\n" -> "one|\n"
-                // - The deleted text is at the end of the text
-                //     e.g. "one [deleted]" -> "one |" -> "one|"
-                // (The pipe | indicates the cursor position.)
-                do {
-                    start -= Character.charCount(codePointBeforeStart);
-                    if (start == 0) break;
-                    codePointBeforeStart = Character.codePointBefore(mText, start);
-                } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
-            } else if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
-                    && (TextUtils.isWhitespace(codePointBeforeStart)
-                            || TextUtils.isPunctuation(codePointBeforeStart))) {
-                // Remove whitespace (except new lines) after the deleted text, in these cases:
-                // - There is punctuation preceding the deleted text
-                //     e.g. "([deleted] two)" -> "(| two)" -> "(|two)"
-                // - There is a new line preceding the deleted text
-                //     e.g. "\n[deleted] two" -> "\n| two" -> "\n|two"
-                // - The deleted text is at the start of the text
-                //     e.g. "[deleted] two" -> "| two" -> "|two"
-                // (The pipe | indicates the cursor position.)
-                do {
-                    end += Character.charCount(codePointAtEnd);
-                    if (end == mText.length()) break;
-                    codePointAtEnd = Character.codePointAt(mText, end);
-                } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
-            }
+            range = adjustHandwritingDeleteGestureRange(range);
         }
 
-        getEditableText().delete(start, end);
-        Selection.setSelection(getEditableText(), start);
+        getEditableText().delete(range[0], range[1]);
+        Selection.setSelection(getEditableText(), range[0]);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
+    public int performHandwritingDeleteRangeGesture(@NonNull DeleteRangeGesture gesture) {
+        int[] startRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getDeletionStartArea()),
+                gesture.getGranularity());
+        if (startRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        int[] endRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getDeletionEndArea()),
+                gesture.getGranularity());
+        if (endRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        int[] range = new int[] {
+                Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1])
+        };
+
+        if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
+            range = adjustHandwritingDeleteGestureRange(range);
+        }
+
+        getEditableText().delete(range[0], range[1]);
+        Selection.setSelection(getEditableText(), range[0]);
+        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+    }
+
+    private int[] adjustHandwritingDeleteGestureRange(int[] range) {
+        // For handwriting delete gestures with word granularity, adjust the start and end offsets
+        // to remove extra whitespace around the deleted text.
+
+        int start = range[0];
+        int end = range[1];
+
+        // If the deleted text is at the start of the text, the behavior is the same as the case
+        // where the deleted text follows a new line character.
+        int codePointBeforeStart = start > 0
+                ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT;
+        // If the deleted text is at the end of the text, the behavior is the same as the case where
+        // the deleted text precedes a new line character.
+        int codePointAtEnd = end < mText.length()
+                ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT;
+
+        if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)
+                && (TextUtils.isWhitespace(codePointAtEnd)
+                        || TextUtils.isPunctuation(codePointAtEnd))) {
+            // Remove whitespace (except new lines) before the deleted text, in these cases:
+            // - There is whitespace following the deleted text
+            //     e.g. "one [deleted] three" -> "one | three" -> "one| three"
+            // - There is punctuation following the deleted text
+            //     e.g. "one [deleted]!" -> "one |!" -> "one|!"
+            // - There is a new line following the deleted text
+            //     e.g. "one [deleted]\n" -> "one |\n" -> "one|\n"
+            // - The deleted text is at the end of the text
+            //     e.g. "one [deleted]" -> "one |" -> "one|"
+            // (The pipe | indicates the cursor position.)
+            do {
+                start -= Character.charCount(codePointBeforeStart);
+                if (start == 0) break;
+                codePointBeforeStart = Character.codePointBefore(mText, start);
+            } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
+            return new int[] {start, end};
+        }
+
+        if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
+                && (TextUtils.isWhitespace(codePointBeforeStart)
+                        || TextUtils.isPunctuation(codePointBeforeStart))) {
+            // Remove whitespace (except new lines) after the deleted text, in these cases:
+            // - There is punctuation preceding the deleted text
+            //     e.g. "([deleted] two)" -> "(| two)" -> "(|two)"
+            // - There is a new line preceding the deleted text
+            //     e.g. "\n[deleted] two" -> "\n| two" -> "\n|two"
+            // - The deleted text is at the start of the text
+            //     e.g. "[deleted] two" -> "| two" -> "|two"
+            // (The pipe | indicates the cursor position.)
+            do {
+                end += Character.charCount(codePointAtEnd);
+                if (end == mText.length()) break;
+                codePointAtEnd = Character.codePointAt(mText, end);
+            } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
+            return new int[] {start, end};
+        }
+
+        // Return the original range.
+        return range;
+    }
+
+    /** @hide */
     public int performHandwritingInsertGesture(@NonNull InsertGesture gesture) {
         PointF point = convertFromScreenToContentCoordinates(gesture.getInsertionPoint());
         int line = getLineForHandwritingGesture(point);
@@ -9431,14 +9497,14 @@
                 lineVerticalCenter + 0.1f,
                 Math.max(startPoint.x, endPoint.x),
                 lineVerticalCenter - 0.1f);
-        Range<Integer> range = mLayout.getRangeForRect(
+        int[] range = mLayout.getRangeForRect(
                 area, new GraphemeClusterSegmentFinder(mText, mTextPaint),
                 Layout.INCLUSION_STRATEGY_ANY_OVERLAP);
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        int startOffset = range.getLower();
-        int endOffset = range.getUpper();
+        int startOffset = range[0];
+        int endOffset = range[1];
         // TODO(b/247557062): This doesn't handle bidirectional text correctly.
 
         Pattern whitespacePattern = getWhitespacePattern();
@@ -9543,7 +9609,7 @@
     }
 
     @Nullable
-    private Range<Integer> getRangeForRect(@NonNull RectF area, int granularity) {
+    private int[] getRangeForRect(@NonNull RectF area, int granularity) {
         SegmentFinder segmentFinder;
         if (granularity == HandwritingGesture.GRANULARITY_WORD) {
             WordIterator wordIterator = getWordIterator();
diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java
index 4a4f561..85b2881 100644
--- a/core/java/android/window/BackEvent.java
+++ b/core/java/android/window/BackEvent.java
@@ -18,8 +18,10 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.view.RemoteAnimationTarget;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -50,6 +52,8 @@
 
     @SwipeEdge
     private final int mSwipeEdge;
+    @Nullable
+    private final RemoteAnimationTarget mDepartingAnimationTarget;
 
     /**
      * Creates a new {@link BackEvent} instance.
@@ -58,12 +62,16 @@
      * @param touchY Absolute Y location of the touch point of this event.
      * @param progress Value between 0 and 1 on how far along the back gesture is.
      * @param swipeEdge Indicates which edge the swipe starts from.
+     * @param departingAnimationTarget The remote animation target of the departing
+     *                                 application window.
      */
-    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge) {
+    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge,
+            @Nullable RemoteAnimationTarget departingAnimationTarget) {
         mTouchX = touchX;
         mTouchY = touchY;
         mProgress = progress;
         mSwipeEdge = swipeEdge;
+        mDepartingAnimationTarget = departingAnimationTarget;
     }
 
     private BackEvent(@NonNull Parcel in) {
@@ -71,6 +79,7 @@
         mTouchY = in.readFloat();
         mProgress = in.readFloat();
         mSwipeEdge = in.readInt();
+        mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
     }
 
     public static final Creator<BackEvent> CREATOR = new Creator<BackEvent>() {
@@ -96,6 +105,7 @@
         dest.writeFloat(mTouchY);
         dest.writeFloat(mProgress);
         dest.writeInt(mSwipeEdge);
+        dest.writeTypedObject(mDepartingAnimationTarget, flags);
     }
 
     /**
@@ -126,6 +136,16 @@
         return mSwipeEdge;
     }
 
+    /**
+     * Returns the {@link RemoteAnimationTarget} of the top departing application window,
+     * or {@code null} if the top window should not be moved for the current type of back
+     * destination.
+     */
+    @Nullable
+    public RemoteAnimationTarget getDepartingAnimationTarget() {
+        return mDepartingAnimationTarget;
+    }
+
     @Override
     public String toString() {
         return "BackEvent{"
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
new file mode 100644
index 0000000..2e3afde
--- /dev/null
+++ b/core/java/android/window/BackProgressAnimator.java
@@ -0,0 +1,139 @@
+/*
+ * 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.window;
+
+import android.util.FloatProperty;
+
+import com.android.internal.dynamicanimation.animation.SpringAnimation;
+import com.android.internal.dynamicanimation.animation.SpringForce;
+
+/**
+ * An animator that drives the predictive back progress with a spring.
+ *
+ * The back gesture's latest touch point and committal state determines the final position of
+ * the spring. The continuous movement of the spring is used to produce {@link BackEvent}s with
+ * smoothly transitioning progress values.
+ *
+ * @hide
+ */
+public class BackProgressAnimator {
+    /**
+     *  A factor to scale the input progress by, so that it works better with the spring.
+     *  We divide the output progress by this value before sending it to apps, so that apps
+     *  always receive progress values in [0, 1].
+     */
+    private static final float SCALE_FACTOR = 100f;
+    private final SpringAnimation mSpring;
+    private ProgressCallback mCallback;
+    private float mProgress = 0;
+    private BackEvent mLastBackEvent;
+    private boolean mStarted = false;
+
+    private void setProgress(float progress) {
+        mProgress = progress;
+    }
+
+    private float getProgress() {
+        return mProgress;
+    }
+
+    private static final FloatProperty<BackProgressAnimator> PROGRESS_PROP =
+            new FloatProperty<BackProgressAnimator>("progress") {
+                @Override
+                public void setValue(BackProgressAnimator animator, float value) {
+                    animator.setProgress(value);
+                    animator.updateProgressValue(value);
+                }
+
+                @Override
+                public Float get(BackProgressAnimator object) {
+                    return object.getProgress();
+                }
+            };
+
+
+    /** A callback to be invoked when there's a progress value update from the animator. */
+    public interface ProgressCallback {
+        /** Called when there's a progress value update. */
+        void onProgressUpdate(BackEvent event);
+    }
+
+    public BackProgressAnimator() {
+        mSpring = new SpringAnimation(this, PROGRESS_PROP);
+        mSpring.setSpring(new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
+    }
+
+    /**
+     * Sets a new target position for the back progress.
+     *
+     * @param event the {@link BackEvent} containing the latest target progress.
+     */
+    public void onBackProgressed(BackEvent event) {
+        if (!mStarted) {
+            return;
+        }
+        mLastBackEvent = event;
+        if (mSpring == null) {
+            return;
+        }
+        mSpring.animateToFinalPosition(event.getProgress() * SCALE_FACTOR);
+    }
+
+    /**
+     * Starts the back progress animation.
+     *
+     * @param event the {@link BackEvent} that started the gesture.
+     * @param callback the back callback to invoke for the gesture. It will receive back progress
+     *                 dispatches as the progress animation updates.
+     */
+    public void onBackStarted(BackEvent event, ProgressCallback callback) {
+        reset();
+        mLastBackEvent = event;
+        mCallback = callback;
+        mStarted = true;
+    }
+
+    /**
+     * Resets the back progress animation. This should be called when back is invoked or cancelled.
+     */
+    public void reset() {
+        mSpring.animateToFinalPosition(0);
+        if (mSpring.canSkipToEnd()) {
+            mSpring.skipToEnd();
+        } else {
+            // Should never happen.
+            mSpring.cancel();
+        }
+        mStarted = false;
+        mLastBackEvent = null;
+        mCallback = null;
+        mProgress = 0;
+    }
+
+    private void updateProgressValue(float progress) {
+        if (mLastBackEvent == null || mCallback == null || !mStarted) {
+            return;
+        }
+        mCallback.onProgressUpdate(
+                new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(),
+                        progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(),
+                        mLastBackEvent.getDepartingAnimationTarget()));
+    }
+
+}
diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl
index 47796de..6af8ddd 100644
--- a/core/java/android/window/IOnBackInvokedCallback.aidl
+++ b/core/java/android/window/IOnBackInvokedCallback.aidl
@@ -28,17 +28,18 @@
 oneway interface IOnBackInvokedCallback {
    /**
     * Called when a back gesture has been started, or back button has been pressed down.
-    * Wraps {@link OnBackInvokedCallback#onBackStarted()}.
+    * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}.
+    *
+    * @param backEvent The {@link BackEvent} containing information about the touch or button press.
     */
-    void onBackStarted();
+    void onBackStarted(in BackEvent backEvent);
 
     /**
      * Called on back gesture progress.
-     * Wraps {@link OnBackInvokedCallback#onBackProgressed()}.
+     * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}.
      *
-     * @param touchX Absolute X location of the touch point.
-     * @param touchY Absolute Y location of the touch point.
-     * @param progress Value between 0 and 1 on how far along the back gesture is.
+     * @param backEvent The {@link BackEvent} containing information about the latest touch point
+     *                  and the progress that the back animation should seek to.
      */
     void onBackProgressed(in BackEvent backEvent);
 
diff --git a/core/java/android/window/OnBackAnimationCallback.java b/core/java/android/window/OnBackAnimationCallback.java
index 1a37e57..c05809b 100644
--- a/core/java/android/window/OnBackAnimationCallback.java
+++ b/core/java/android/window/OnBackAnimationCallback.java
@@ -13,14 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package android.window;
-
 import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.Dialog;
 import android.view.View;
-
 /**
  * Interface for applications to register back animation callbacks along their custom back
  * handling.
@@ -40,11 +37,10 @@
  * @hide
  */
 public interface OnBackAnimationCallback extends OnBackInvokedCallback {
-   /**
-    * Called when a back gesture has been started, or back button has been pressed down.
-    */
+    /**
+     * Called when a back gesture has been started, or back button has been pressed down.
+     */
     default void onBackStarted() { }
-
     /**
      * Called on back gesture progress.
      *
@@ -53,7 +49,6 @@
      * @see BackEvent
      */
     default void onBackProgressed(@NonNull BackEvent backEvent) { }
-
     /**
      * Called when a back gesture or back button press has been cancelled.
      */
diff --git a/core/java/android/window/OnBackInvokedCallback.java b/core/java/android/window/OnBackInvokedCallback.java
index 6e2d4f9..62c41bf 100644
--- a/core/java/android/window/OnBackInvokedCallback.java
+++ b/core/java/android/window/OnBackInvokedCallback.java
@@ -16,6 +16,7 @@
 
 package android.window;
 
+import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.Dialog;
 import android.view.Window;
@@ -41,8 +42,35 @@
 @SuppressWarnings("deprecation")
 public interface OnBackInvokedCallback {
     /**
+     * Called when a back gesture has been started, or back button has been pressed down.
+     *
+     * @param backEvent The {@link BackEvent} containing information about the touch or
+     *                  button press.
+     *
+     * @hide
+     */
+    default void onBackStarted(@NonNull BackEvent backEvent) {}
+
+    /**
+     * Called when a back gesture has been progressed.
+     *
+     * @param backEvent The {@link BackEvent} containing information about the latest touch point
+     *                  and the progress that the back animation should seek to.
+     *
+     * @hide
+     */
+    default void onBackProgressed(@NonNull BackEvent backEvent) {}
+
+    /**
      * Called when a back gesture has been completed and committed, or back button pressed
      * has been released and committed.
      */
     void onBackInvoked();
+
+    /**
+     * Called when a back gesture or button press has been cancelled.
+     *
+     * @hide
+     */
+    default void onBackCancelled() {}
 }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 0730f3d..fda39c1 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -218,19 +218,24 @@
     public Checker getChecker() {
         return mChecker;
     }
+    @NonNull
+    private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
 
     static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
         private final WeakReference<OnBackInvokedCallback> mCallback;
+
         OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) {
             mCallback = new WeakReference<>(callback);
         }
 
         @Override
-        public void onBackStarted() {
+        public void onBackStarted(BackEvent backEvent) {
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
-                    callback.onBackStarted();
+                    mProgressAnimator.onBackStarted(backEvent, event ->
+                            callback.onBackProgressed(event));
+                    callback.onBackStarted(backEvent);
                 }
             });
         }
@@ -240,7 +245,7 @@
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
-                    callback.onBackProgressed(backEvent);
+                    mProgressAnimator.onBackProgressed(backEvent);
                 }
             });
         }
@@ -248,6 +253,7 @@
         @Override
         public void onBackCancelled() {
             Handler.getMain().post(() -> {
+                mProgressAnimator.reset();
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
                     callback.onBackCancelled();
@@ -258,6 +264,7 @@
         @Override
         public void onBackInvoked() throws RemoteException {
             Handler.getMain().post(() -> {
+                mProgressAnimator.reset();
                 final OnBackInvokedCallback callback = mCallback.get();
                 if (callback == null) {
                     return;
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 4a1f7eb..42b46cd 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -647,15 +647,16 @@
 
         if (info instanceof DisplayResolveInfo) {
             DisplayResolveInfo dri = (DisplayResolveInfo) info;
-            boolean hasLabel = dri.hasDisplayLabel();
-            holder.bindLabel(
-                    dri.getDisplayLabel(),
-                    dri.getExtendedInfo(),
-                    hasLabel && alwaysShowSubLabel());
-            holder.bindIcon(info);
-            if (!hasLabel) {
+            if (dri.hasDisplayLabel()) {
+                holder.bindLabel(
+                        dri.getDisplayLabel(),
+                        dri.getExtendedInfo(),
+                        alwaysShowSubLabel());
+            } else {
+                holder.bindLabel("", "", false);
                 loadLabel(dri);
             }
+            holder.bindIcon(info);
             if (!dri.hasDisplayIcon()) {
                 loadIcon(dri);
             }
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index e748982..8e7207f 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -45,6 +45,7 @@
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId);
     int[] getAppWidgetIdsForHost(String callingPackage, int hostId);
+    void setAppWidgetHidden(in String callingPackage, int hostId);
     IntentSender createAppWidgetConfigIntentSender(String callingPackage, int appWidgetId,
             int intentFlags);
 
diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl
index f09e176..21c7baa 100644
--- a/core/java/com/android/internal/backup/IBackupTransport.aidl
+++ b/core/java/com/android/internal/backup/IBackupTransport.aidl
@@ -16,6 +16,7 @@
 
 package com.android.internal.backup;
 
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
 import android.app.backup.RestoreSet;
 import android.content.Intent;
@@ -400,4 +401,13 @@
      * <p>For supported flags see {@link android.app.backup.BackupAgent}.
      */
     void getTransportFlags(in AndroidFuture<int> resultFuture);
+
+    /**
+     * Ask the transport for a {@link IBackupManagerMonitor} instance which will be used by the
+     * framework to report logging events back to the transport.
+     *
+     * Backups requested from outside the framework may pass in a monitor with the request,
+     * however backups initiated by the framework will call this method to retrieve one.
+     */
+    void getBackupManagerMonitor(in AndroidFuture<IBackupManagerMonitor> resultFuture);
 }
diff --git a/core/java/com/android/internal/inputmethod/EditableInputConnection.java b/core/java/com/android/internal/inputmethod/EditableInputConnection.java
index f260d7d..f600c36 100644
--- a/core/java/com/android/internal/inputmethod/EditableInputConnection.java
+++ b/core/java/com/android/internal/inputmethod/EditableInputConnection.java
@@ -35,6 +35,7 @@
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.DeleteGesture;
+import android.view.inputmethod.DeleteRangeGesture;
 import android.view.inputmethod.DumpableInputConnection;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
@@ -44,6 +45,7 @@
 import android.view.inputmethod.JoinOrSplitGesture;
 import android.view.inputmethod.RemoveSpaceGesture;
 import android.view.inputmethod.SelectGesture;
+import android.view.inputmethod.SelectRangeGesture;
 import android.widget.TextView;
 
 import java.util.concurrent.Executor;
@@ -275,8 +277,12 @@
         int result;
         if (gesture instanceof SelectGesture) {
             result = mTextView.performHandwritingSelectGesture((SelectGesture) gesture);
+        } else if (gesture instanceof SelectRangeGesture) {
+            result = mTextView.performHandwritingSelectRangeGesture((SelectRangeGesture) gesture);
         } else if (gesture instanceof DeleteGesture) {
             result = mTextView.performHandwritingDeleteGesture((DeleteGesture) gesture);
+        } else if (gesture instanceof DeleteRangeGesture) {
+            result = mTextView.performHandwritingDeleteRangeGesture((DeleteRangeGesture) gesture);
         } else if (gesture instanceof InsertGesture) {
             result = mTextView.performHandwritingInsertGesture((InsertGesture) gesture);
         } else if (gesture instanceof RemoveSpaceGesture) {
diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
index 09c97b3..1b4afd6 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
@@ -49,6 +49,8 @@
                 return "WINDOW_FOCUS_GAIN";
             case StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY:
                 return "WINDOW_FOCUS_GAIN_REPORT_ONLY";
+            case StartInputReason.SCHEDULED_CHECK_FOCUS:
+                return "SCHEDULED_CHECK_FOCUS";
             case StartInputReason.APP_CALLED_RESTART_INPUT_API:
                 return "APP_CALLED_RESTART_INPUT_API";
             case StartInputReason.CHECK_FOCUS:
diff --git a/core/java/com/android/internal/inputmethod/StartInputReason.java b/core/java/com/android/internal/inputmethod/StartInputReason.java
index 51ed841..733d975 100644
--- a/core/java/com/android/internal/inputmethod/StartInputReason.java
+++ b/core/java/com/android/internal/inputmethod/StartInputReason.java
@@ -31,6 +31,7 @@
         StartInputReason.UNSPECIFIED,
         StartInputReason.WINDOW_FOCUS_GAIN,
         StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY,
+        StartInputReason.SCHEDULED_CHECK_FOCUS,
         StartInputReason.APP_CALLED_RESTART_INPUT_API,
         StartInputReason.CHECK_FOCUS,
         StartInputReason.BOUND_TO_IMMS,
@@ -58,6 +59,11 @@
      */
     int WINDOW_FOCUS_GAIN_REPORT_ONLY = 2;
     /**
+     * Similar to {@link #CHECK_FOCUS}, but the one scheduled with
+     * {@link android.view.ViewRootImpl#dispatchCheckFocus()}.
+     */
+    int SCHEDULED_CHECK_FOCUS = 3;
+    /**
      * {@link android.view.inputmethod.InputMethodManager#restartInput(android.view.View)} is
      * either explicitly called by the application or indirectly called by some Framework class
      * (e.g. {@link android.widget.EditText}).
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 76f33a6..b0d5922 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -45,6 +45,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_ENTER_TRANSITION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_EXIT_TRANSITION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF_SHOW_AOD;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL;
@@ -224,6 +225,7 @@
     public static final int CUJ_SHADE_CLEAR_ALL = 62;
     public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63;
     public static final int CUJ_LOCKSCREEN_OCCLUSION = 64;
+    public static final int CUJ_RECENTS_SCROLLING = 65;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -297,6 +299,7 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING,
     };
 
     private static class InstanceHolder {
@@ -385,7 +388,8 @@
             CUJ_TASKBAR_COLLAPSE,
             CUJ_SHADE_CLEAR_ALL,
             CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
-            CUJ_LOCKSCREEN_OCCLUSION
+            CUJ_LOCKSCREEN_OCCLUSION,
+            CUJ_RECENTS_SCROLLING
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -900,6 +904,8 @@
                 return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION";
             case CUJ_LOCKSCREEN_OCCLUSION:
                 return "LOCKSCREEN_OCCLUSION";
+            case CUJ_RECENTS_SCROLLING:
+                return "RECENTS_SCROLLING";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 681b46a..0489dc81 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -35,7 +35,10 @@
 
 // Manages the NotificationChannels used by the frameworks itself.
 public class SystemNotificationChannels {
-    public static String VIRTUAL_KEYBOARD  = "VIRTUAL_KEYBOARD";
+    /**
+     * @deprecated Legacy system channel, which is no longer used,
+     */
+    @Deprecated public static String VIRTUAL_KEYBOARD  = "VIRTUAL_KEYBOARD";
     public static String PHYSICAL_KEYBOARD = "PHYSICAL_KEYBOARD";
     public static String SECURITY = "SECURITY";
     public static String CAR_MODE = "CAR_MODE";
@@ -72,13 +75,6 @@
     public static void createAll(Context context) {
         final NotificationManager nm = context.getSystemService(NotificationManager.class);
         List<NotificationChannel> channelsList = new ArrayList<NotificationChannel>();
-        final NotificationChannel keyboard = new NotificationChannel(
-                VIRTUAL_KEYBOARD,
-                context.getString(R.string.notification_channel_virtual_keyboard),
-                NotificationManager.IMPORTANCE_LOW);
-        keyboard.setBlockable(true);
-        channelsList.add(keyboard);
-
         final NotificationChannel physicalKeyboardChannel = new NotificationChannel(
                 PHYSICAL_KEYBOARD,
                 context.getString(R.string.notification_channel_physical_keyboard),
@@ -237,6 +233,7 @@
     /** Remove notification channels which are no longer used */
     public static void removeDeprecated(Context context) {
         final NotificationManager nm = context.getSystemService(NotificationManager.class);
+        nm.deleteNotificationChannel(VIRTUAL_KEYBOARD);
         nm.deleteNotificationChannel(DEVICE_ADMIN_DEPRECATED);
         nm.deleteNotificationChannel(SYSTEM_CHANGES_DEPRECATED);
     }
diff --git a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
index a09c823..04dd2d7 100644
--- a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
+++ b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
@@ -97,7 +97,6 @@
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
@@ -224,7 +223,6 @@
             }
 
             case KeyEvent.KEYCODE_HEADSETHOOK:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 295dc54..25ac1bd 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -41,12 +41,16 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ColorSpace;
 import android.graphics.Picture;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.hardware.HardwareBuffer;
+import android.media.Image;
+import android.media.ImageReader;
 import android.os.SystemProperties;
 import android.util.Slog;
+import android.view.SurfaceControl;
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManager.TransitionOldType;
 import android.view.WindowManager.TransitionType;
@@ -59,9 +63,11 @@
 import android.view.animation.PathInterpolator;
 import android.view.animation.ScaleAnimation;
 import android.view.animation.TranslateAnimation;
+import android.window.ScreenCapture;
 
 import com.android.internal.R;
 
+import java.nio.ByteBuffer;
 import java.util.List;
 
 /** @hide */
@@ -1262,4 +1268,90 @@
 
         return set;
     }
+
+    /** Returns whether the hardware buffer passed in is marked as protected. */
+    public static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
+        return (hardwareBuffer.getUsage() & HardwareBuffer.USAGE_PROTECTED_CONTENT)
+                == HardwareBuffer.USAGE_PROTECTED_CONTENT;
+    }
+
+    /** Returns the luminance in 0~1. */
+    public static float getBorderLuma(SurfaceControl surfaceControl, int w, int h) {
+        final ScreenCapture.ScreenshotHardwareBuffer buffer =
+                ScreenCapture.captureLayers(surfaceControl, new Rect(0, 0, w, h), 1);
+        if (buffer != null) {
+            return getBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
+        }
+        return 0;
+    }
+
+    /** Returns the luminance in 0~1. */
+    public static float getBorderLuma(HardwareBuffer hwBuffer, ColorSpace colorSpace) {
+        if (hwBuffer == null) {
+            return 0;
+        }
+        final int format = hwBuffer.getFormat();
+        // Only support RGB format in 4 bytes. And protected buffer is not readable.
+        if (format != HardwareBuffer.RGBA_8888 || hasProtectedContent(hwBuffer)) {
+            return 0;
+        }
+
+        final ImageReader ir = ImageReader.newInstance(hwBuffer.getWidth(), hwBuffer.getHeight(),
+                format, 1 /* maxImages */);
+        ir.getSurface().attachAndQueueBufferWithColorSpace(hwBuffer, colorSpace);
+        final Image image = ir.acquireLatestImage();
+        if (image == null || image.getPlaneCount() < 1) {
+            return 0;
+        }
+
+        final Image.Plane plane = image.getPlanes()[0];
+        final ByteBuffer buffer = plane.getBuffer();
+        final int width = image.getWidth();
+        final int height = image.getHeight();
+        final int pixelStride = plane.getPixelStride();
+        final int rowStride = plane.getRowStride();
+        final int sampling = 10;
+        final int[] borderLumas = new int[(width + height) * 2 / sampling];
+
+        // Grab the top and bottom borders.
+        int i = 0;
+        for (int x = 0, size = width - sampling; x < size; x += sampling) {
+            borderLumas[i++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
+            borderLumas[i++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
+        }
+
+        // Grab the left and right borders.
+        for (int y = 0, size = height - sampling; y < size; y += sampling) {
+            borderLumas[i++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
+            borderLumas[i++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
+        }
+
+        ir.close();
+
+        // Get "mode" by histogram.
+        final int[] histogram = new int[256];
+        int maxCount = 0;
+        int mostLuma = 0;
+        for (int luma : borderLumas) {
+            final int count = ++histogram[luma];
+            if (count > maxCount) {
+                maxCount = count;
+                mostLuma = luma;
+            }
+        }
+        return mostLuma / 255f;
+    }
+
+    /** Returns the luminance of the pixel in 0~255. */
+    private static int getPixelLuminance(ByteBuffer buffer, int x, int y, int pixelStride,
+            int rowStride) {
+        final int color = buffer.getInt(y * rowStride + x * pixelStride);
+        // The buffer from ImageReader is always in native order (little-endian), so extract the
+        // color components in reversed order.
+        final int r = color & 0xff;
+        final int g = (color >> 8) & 0xff;
+        final int b = (color >> 16) & 0xff;
+        // Approximation of WCAG 2.0 relative luminance.
+        return ((r * 8) + (g * 22) + (b * 2)) >> 5;
+    }
 }
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 44cfe1a..1d4b246 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -322,4 +322,7 @@
 
     /** Unregisters a nearby media devices provider. */
     void unregisterNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider);
+
+    /** Dump protos from SystemUI. The proto definition is defined there */
+    void dumpProto(in String[] args, in ParcelFileDescriptor pfd);
 }
diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp
index 1520ea5..0381510 100644
--- a/core/jni/android_graphics_BLASTBufferQueue.cpp
+++ b/core/jni/android_graphics_BLASTBufferQueue.cpp
@@ -71,10 +71,12 @@
         }
     }
 
-    void onTransactionHang(bool isGpuHang) {
+    void onTransactionHang(const std::string& reason) {
         if (mTransactionHangObject) {
+            JNIEnv* env = getenv(mVm);
+            ScopedLocalRef<jstring> jReason(env, env->NewStringUTF(reason.c_str()));
             getenv(mVm)->CallVoidMethod(mTransactionHangObject,
-                                        gTransactionHangCallback.onTransactionHang, isGpuHang);
+                                        gTransactionHangCallback.onTransactionHang, jReason.get());
         }
     }
 
@@ -177,7 +179,7 @@
     sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
     return queue->isSameSurfaceControl(reinterpret_cast<SurfaceControl*>(surfaceControl));
 }
-  
+
 static void nativeSetTransactionHangCallback(JNIEnv* env, jclass clazz, jlong ptr,
                                              jobject transactionHangCallback) {
     sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
@@ -186,9 +188,8 @@
     } else {
         sp<TransactionHangCallbackWrapper> wrapper =
                 new TransactionHangCallbackWrapper{env, transactionHangCallback};
-        queue->setTransactionHangCallback([wrapper](bool isGpuHang) {
-            wrapper->onTransactionHang(isGpuHang);
-        });
+        queue->setTransactionHangCallback(
+                [wrapper](const std::string& reason) { wrapper->onTransactionHang(reason); });
     }
 }
 
@@ -236,7 +237,8 @@
     jclass transactionHangClass =
             FindClassOrDie(env, "android/graphics/BLASTBufferQueue$TransactionHangCallback");
     gTransactionHangCallback.onTransactionHang =
-            GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang", "(Z)V");
+            GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang",
+                             "(Ljava/lang/String;)V");
 
     return 0;
 }
diff --git a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
index 09f3a72..2437a51 100644
--- a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
+++ b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
@@ -89,6 +89,24 @@
 
 extern "C" {
 
+static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) {
+    ALOGV("nativeDetectSurfaceType");
+    sp<ANativeWindow> anw;
+    if ((anw = getNativeWindow(env, surface)) == NULL) {
+        ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__);
+        return BAD_VALUE;
+    }
+    int32_t fmt = 0;
+    status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &fmt);
+    if (err != NO_ERROR) {
+        ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__,
+              strerror(-err), err);
+        OVERRIDE_SURFACE_ERROR(err);
+        return err;
+    }
+    return fmt;
+}
+
 static jint SurfaceUtils_nativeDetectSurfaceDataspace(JNIEnv* env, jobject thiz, jobject surface) {
     ALOGV("nativeDetectSurfaceDataspace");
     sp<ANativeWindow> anw;
@@ -107,27 +125,6 @@
     return fmt;
 }
 
-static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) {
-    ALOGV("nativeDetectSurfaceType");
-    sp<ANativeWindow> anw;
-    if ((anw = getNativeWindow(env, surface)) == NULL) {
-        ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__);
-        return BAD_VALUE;
-    }
-    int32_t halFmt = 0;
-    status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &halFmt);
-    if (err != NO_ERROR) {
-        ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__,
-              strerror(-err), err);
-        OVERRIDE_SURFACE_ERROR(err);
-        return err;
-    }
-    int32_t dataspace = SurfaceUtils_nativeDetectSurfaceDataspace(env, thiz, surface);
-    int32_t fmt = static_cast<int32_t>(
-            mapHalFormatDataspaceToPublicFormat(halFmt, static_cast<android_dataspace>(dataspace)));
-    return fmt;
-}
-
 static jint SurfaceUtils_nativeDetectSurfaceDimens(JNIEnv* env, jobject thiz, jobject surface,
                                                    jintArray dimens) {
     ALOGV("nativeGetSurfaceDimens");
diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp
index 40f6e4f..5c71f69 100644
--- a/core/jni/fd_utils.cpp
+++ b/core/jni/fd_utils.cpp
@@ -580,6 +580,7 @@
       // TODO(narayan): This will be an error in a future android release.
       // error = true;
       // ALOGW("Zygote closed file descriptor %d.", it->first);
+      delete it->second;
       it = open_fd_map_.erase(it);
     } else {
       // The entry from the file descriptor table is still open. Restat
diff --git a/core/proto/android/view/imefocuscontroller.proto b/core/proto/android/view/imefocuscontroller.proto
index ff9dee6..ccde9b7 100644
--- a/core/proto/android/view/imefocuscontroller.proto
+++ b/core/proto/android/view/imefocuscontroller.proto
@@ -25,6 +25,6 @@
  */
 message ImeFocusControllerProto {
     optional bool has_ime_focus = 1;
-    optional string served_view = 2;
-    optional string next_served_view = 3;
+    optional string served_view = 2 [deprecated = true];
+    optional string next_served_view = 3 [deprecated = true];
 }
\ No newline at end of file
diff --git a/core/proto/android/view/inputmethod/inputmethodmanager.proto b/core/proto/android/view/inputmethod/inputmethodmanager.proto
index 9fed0ef..ea5f1e8 100644
--- a/core/proto/android/view/inputmethod/inputmethodmanager.proto
+++ b/core/proto/android/view/inputmethod/inputmethodmanager.proto
@@ -29,4 +29,6 @@
     optional int32 display_id = 3;
     optional bool active = 4;
     optional bool served_connecting = 5;
+    optional string served_view = 6;
+    optional string next_served_view = 7;
 }
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 1f23eb6..554b153 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -668,7 +668,6 @@
     <protected-broadcast android:name="android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED" />
     <protected-broadcast android:name="android.media.tv.action.WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED" />
     <protected-broadcast android:name="android.media.tv.action.CHANNEL_BROWSABLE_REQUESTED" />
-    <protected-broadcast android:name="com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER" />
 
     <!-- Time zone rules update intents fired by the system server -->
     <protected-broadcast android:name="com.android.intent.action.timezone.RULES_UPDATE_OPERATION" />
@@ -1152,7 +1151,28 @@
                 android:protectionLevel="dangerous" />
 
     <!-- Allows an application to write to external storage.
-         <p class="note"><strong>Note:</strong> If <em>both</em> your <a
+         <p><strong>Note: </strong>If your app targets {@link android.os.Build.VERSION_CODES#R} or
+         higher, this permission has no effect.
+
+         <p>If your app is on a device that runs API level 19 or higher, you don't need to declare
+         this permission to read and write files in your application-specific directories returned
+         by {@link android.content.Context#getExternalFilesDir} and
+         {@link android.content.Context#getExternalCacheDir}.
+
+         <p>Learn more about how to
+         <a href="{@docRoot}training/data-storage/shared/media#update-other-apps-files">modify media
+         files</a> that your app doesn't own, and how to
+         <a href="{@docRoot}training/data-storage/shared/documents-files">modify non-media files</a>
+         that your app doesn't own.
+
+         <p>If your app is a file manager and needs broad access to external storage files, then
+         the system must place your app on an allowlist so that you can successfully request the
+         <a href="#MANAGE_EXTERNAL_STORAGE><code>MANAGE_EXTERNAL_STORAGE</code></a> permission.
+         Learn more about the appropriate use cases for
+         <a href="{@docRoot}training/data-storage/manage-all-files>managing all files on a storage
+         device</a>.
+
+         <p>If <em>both</em> your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code
          minSdkVersion}</a> and <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
@@ -1160,12 +1180,6 @@
          grants your app this permission. If you don't need this permission, be sure your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
          targetSdkVersion}</a> is 4 or higher.
-         <p>Starting in API level 19, this permission is <em>not</em> required to
-         read/write files in your application-specific directories returned by
-         {@link android.content.Context#getExternalFilesDir} and
-         {@link android.content.Context#getExternalCacheDir}.
-         <p>If this permission is not allowlisted for an app that targets an API level before
-         {@link android.os.Build.VERSION_CODES#Q} this permission cannot be granted to apps.</p>
          <p>Protection level: dangerous</p>
     -->
     <permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -3043,6 +3057,12 @@
     <permission android:name="android.permission.CREATE_USERS"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi @hide Allows an application to set user association
+         with a certain subscription. Used by Enterprise to associate a
+         subscription with a work or personal profile. -->
+    <permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION"
+                android:protectionLevel="signature" />
+
     <!-- @SystemApi @hide Allows an application to call APIs that allow it to query users on the
          device. -->
     <permission android:name="android.permission.QUERY_USERS"
@@ -3156,6 +3176,13 @@
 
     <!-- Allows an application to call
         {@link android.app.ActivityManager#killBackgroundProcesses}.
+        <p>As of Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+        the {@link android.app.ActivityManager#killBackgroundProcesses} is no longer available to
+        third party applications. For backwards compatibility, the background processes of the
+        caller's own package will still be killed when calling this API. If the caller has
+        the system permission {@code KILL_ALL_BACKGROUND_PROCESSES}, other processes will be
+        killed too.
+
          <p>Protection level: normal
     -->
     <permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"
@@ -3163,6 +3190,16 @@
         android:description="@string/permdesc_killBackgroundProcesses"
         android:protectionLevel="normal" />
 
+    <!-- @SystemApi @hide Allows an application to call
+        {@link android.app.ActivityManager#killBackgroundProcesses}
+        to kill background processes of other apps.
+         <p>Not for use by third-party applications.
+    -->
+    <permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"
+        android:label="@string/permlab_killBackgroundProcesses"
+        android:description="@string/permdesc_killBackgroundProcesses"
+        android:protectionLevel="signature|privileged" />
+
     <!-- @SystemApi @hide Allows an application to query process states and current
          OOM adjustment scores.
          <p>Not for use by third-party applications. -->
@@ -4261,6 +4298,13 @@
     <permission android:name="android.permission.BIND_AUTOFILL_SERVICE"
         android:protectionLevel="signature" />
 
+    <!-- Must be required by a CredentialProviderService to ensure that only the
+         system can bind to it.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
+                android:protectionLevel="signature" />
+
    <!-- Alternative version of android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE.
         This permission was renamed during the O previews but it was supported on the final O
         release, so we need to carry it over.
@@ -6571,6 +6615,13 @@
                 android:protectionLevel="signature" />
     <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" />
 
+    <!-- Allows financed device kiosk apps to perform actions on the Device Lock service
+         <p>Protection level: internal|role
+         <p>Intended for use by the FINANCED_DEVICE_KIOSK role only.
+    -->
+    <permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE"
+                android:protectionLevel="internal|role" />
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/core/res/res/anim/dream_activity_close_exit.xml b/core/res/res/anim/dream_activity_close_exit.xml
index c4599da..8df624f 100644
--- a/core/res/res/anim/dream_activity_close_exit.xml
+++ b/core/res/res/anim/dream_activity_close_exit.xml
@@ -19,5 +19,5 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="1.0"
     android:toAlpha="0.0"
-    android:duration="100" />
+    android:duration="@integer/config_dreamCloseAnimationDuration" />
 
diff --git a/core/res/res/anim/dream_activity_open_enter.xml b/core/res/res/anim/dream_activity_open_enter.xml
index 9e1c6e2..d6d9c5c 100644
--- a/core/res/res/anim/dream_activity_open_enter.xml
+++ b/core/res/res/anim/dream_activity_open_enter.xml
@@ -22,5 +22,5 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="0.0"
     android:toAlpha="1.0"
-    android:duration="1000" />
+    android:duration="@integer/config_dreamOpenAnimationDuration" />
 
diff --git a/core/res/res/anim/dream_activity_open_exit.xml b/core/res/res/anim/dream_activity_open_exit.xml
index 740f528..2c2e501 100644
--- a/core/res/res/anim/dream_activity_open_exit.xml
+++ b/core/res/res/anim/dream_activity_open_exit.xml
@@ -22,4 +22,4 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="1.0"
     android:toAlpha="1.0"
-    android:duration="1000" />
+    android:duration="@integer/config_dreamOpenAnimationDuration" />
diff --git a/core/res/res/drawable-hdpi/ic_notification_ime_default.png b/core/res/res/drawable-hdpi/ic_notification_ime_default.png
deleted file mode 100644
index 369c88d..0000000
--- a/core/res/res/drawable-hdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-mdpi/ic_notification_ime_default.png b/core/res/res/drawable-mdpi/ic_notification_ime_default.png
deleted file mode 100644
index 7d97eb5..0000000
--- a/core/res/res/drawable-mdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/ic_notification_ime_default.png b/core/res/res/drawable-xhdpi/ic_notification_ime_default.png
deleted file mode 100644
index 900801a..0000000
--- a/core/res/res/drawable-xhdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png b/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png
deleted file mode 100644
index 6c8222e..0000000
--- a/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index a7f2aa7..be1c939 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -24,6 +24,7 @@
     android:gravity="center_vertical"
     android:orientation="horizontal"
     android:theme="@style/Theme.DeviceDefault.Notification"
+    android:importantForAccessibility="no"
     >
 
     <ImageView
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b5cdcff..173908d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2447,6 +2447,11 @@
     <!-- Whether dreams are disabled when ambient mode is suppressed. -->
     <bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool>
 
+    <!-- The duration in milliseconds of the dream opening animation.  -->
+    <integer name="config_dreamOpenAnimationDuration">250</integer>
+    <!-- The duration in milliseconds of the dream closing animation.  -->
+    <integer name="config_dreamCloseAnimationDuration">100</integer>
+
     <!-- Whether to dismiss the active dream when an activity is started. Doesn't apply to
          assistant activities (ACTIVITY_TYPE_ASSISTANT) -->
     <bool name="config_dismissDreamOnActivityStart">false</bool>
@@ -3559,9 +3564,9 @@
          config_sidefpsSkipWaitForPowerVendorAcquireMessage -->
     <integer name="config_sidefpsSkipWaitForPowerAcquireMessage">6</integer>
 
-    <!-- This vendor acquired message that will cause the sidefpsKgPowerPress window to be skipped.
-         config_sidefpsSkipWaitForPowerOnFingerUp must be true and
-         config_sidefpsSkipWaitForPowerAcquireMessage must be BIOMETRIC_ACQUIRED_VENDOR == 6. -->
+    <!-- This vendor acquired message will cause the sidefpsKgPowerPress window to be skipped
+         when config_sidefpsSkipWaitForPowerAcquireMessage == 6 (VENDOR) and the vendor acquire
+         message equals this constant -->
     <integer name="config_sidefpsSkipWaitForPowerVendorAcquireMessage">2</integer>
 
     <!-- This config is used to force VoiceInteractionService to start on certain low ram devices.
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index a1d73ff..71b2f00 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -117,4 +117,17 @@
     <!-- Whether using the new SubscriptionManagerService or the old SubscriptionController -->
     <bool name="config_using_subscription_manager_service">false</bool>
     <java-symbol type="bool" name="config_using_subscription_manager_service" />
+
+    <!-- Boolean indicating whether the emergency numbers for a country, sourced from modem/config,
+         should be ignored if that country is 'locked' (i.e. ignore_modem_config set to true) in
+         Android Emergency DB. If this value is true, emergency numbers for a country, sourced from
+         modem/config, will be ignored if that country is 'locked' in Android Emergency DB. -->
+    <bool name="ignore_modem_config_emergency_numbers">false</bool>
+    <java-symbol type="bool" name="ignore_modem_config_emergency_numbers" />
+
+    <!-- Boolean indicating whether emergency numbers routing from the android emergency number
+         database should be ignored (i.e. routing will always be set to UNKNOWN). If this value is
+         true, routing from the android emergency number database will be ignored. -->
+    <bool name="ignore_emergency_number_routing_from_db">false</bool>
+    <java-symbol type="bool" name="ignore_emergency_number_routing_from_db" />
 </resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 5f99113..d0fca8b 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -741,9 +741,6 @@
     <!-- Text shown in place of notification contents when the notification is hidden on a secure lockscreen -->
     <string name="notification_hidden_text">New notification</string>
 
-    <!-- Text shown when viewing channel settings for notifications related to the virtual keyboard -->
-    <string name="notification_channel_virtual_keyboard">Virtual keyboard</string>
-
     <!-- Text shown when viewing channel settings for notifications related to the hardware keyboard -->
     <string name="notification_channel_physical_keyboard">Physical keyboard</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b94d799..29ec347 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1992,7 +1992,6 @@
   <java-symbol type="color" name="config_defaultNotificationColor" />
   <java-symbol type="color" name="decor_view_status_guard" />
   <java-symbol type="color" name="decor_view_status_guard_light" />
-  <java-symbol type="drawable" name="ic_notification_ime_default" />
   <java-symbol type="drawable" name="ic_menu_refresh" />
   <java-symbol type="drawable" name="ic_settings" />
   <java-symbol type="drawable" name="ic_voice_search" />
@@ -2237,6 +2236,8 @@
   <java-symbol type="string" name="config_dreamsDefaultComponent" />
   <java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" />
   <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" />
+  <java-symbol type="integer" name="config_dreamOpenAnimationDuration" />
+  <java-symbol type="integer" name="config_dreamCloseAnimationDuration" />
   <java-symbol type="array" name="config_supportedDreamComplications" />
   <java-symbol type="array" name="config_disabledDreamComponents" />
   <java-symbol type="bool" name="config_dismissDreamOnActivityStart" />
@@ -3724,7 +3725,6 @@
   <java-symbol type="integer" name="config_maxUiWidth" />
 
   <!-- system notification channels -->
-  <java-symbol type="string" name="notification_channel_virtual_keyboard" />
   <java-symbol type="string" name="notification_channel_physical_keyboard" />
   <java-symbol type="string" name="notification_channel_security" />
   <java-symbol type="string" name="notification_channel_car_mode" />
diff --git a/core/tests/BroadcastRadioTests/Android.bp b/core/tests/BroadcastRadioTests/Android.bp
index 113f45d..7cb64c8 100644
--- a/core/tests/BroadcastRadioTests/Android.bp
+++ b/core/tests/BroadcastRadioTests/Android.bp
@@ -23,23 +23,32 @@
 
 android_test {
     name: "BroadcastRadioTests",
+    srcs: ["src/**/*.java"],
     privileged: true,
     certificate: "platform",
     // TODO(b/13282254): uncomment when b/13282254 is fixed
     // sdk_version: "current"
     platform_apis: true,
-    static_libs: [
-        "compatibility-device-util-axt",
-        "androidx.test.rules",
-        "testng",
-        "services.core",
-    ],
-    libs: ["android.test.base"],
-    srcs: ["src/**/*.java"],
     dex_preopt: {
         enabled: false,
     },
     optimize: {
         enabled: false,
     },
+    static_libs: [
+        "services.core",
+        "androidx.test.rules",
+        "truth-prebuilt",
+        "testng",
+        "mockito-target-extended",
+    ],
+    libs: ["android.test.base"],
+    test_suites: [
+        "general-tests",
+    ],
+    // mockito-target-inline dependency
+    jni_libs: [
+        "libcarservicejni",
+        "libdexmakerjvmtiagent",
+    ],
 }
diff --git a/core/tests/BroadcastRadioTests/AndroidManifest.xml b/core/tests/BroadcastRadioTests/AndroidManifest.xml
index ce12cc9..869b484 100644
--- a/core/tests/BroadcastRadioTests/AndroidManifest.xml
+++ b/core/tests/BroadcastRadioTests/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
     <uses-permission android:name="android.permission.ACCESS_BROADCAST_RADIO" />
 
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
index 11eb158..3f35e99 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
@@ -33,11 +33,9 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
-import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
@@ -47,6 +45,7 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -56,8 +55,7 @@
 /**
  * A test for broadcast radio API.
  */
-@RunWith(AndroidJUnit4.class)
-@MediumTest
+@RunWith(MockitoJUnitRunner.class)
 public class RadioTunerTest {
     private static final String TAG = "BroadcastRadioTests.RadioTuner";
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
new file mode 100644
index 0000000..42143b9
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.hardware.radio.tests.unittests;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.ProgramSelector;
+import android.util.ArrayMap;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+public final class RadioAnnouncementTest {
+    private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier(
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 90500);
+    private static final ProgramSelector FM_PROGRAM_SELECTOR = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER, /* secondaryIds= */ null,
+            /* vendorIds= */ null);
+    private static final int TRAFFIC_ANNOUNCEMENT_TYPE = Announcement.TYPE_TRAFFIC;
+    private static final Map<String, String> VENDOR_INFO = createVendorInfo();
+    private static final Announcement TEST_ANNOUNCEMENT =
+            new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO);
+
+    @Test
+    public void constructor_withNullSelector_fails() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            new Announcement(/* selector= */ null, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO);
+        });
+
+        assertWithMessage("Exception for null program selector in announcement constructor")
+                .that(thrown).hasMessageThat().contains("Program selector cannot be null");
+    }
+
+    @Test
+    public void constructor_withNullVendorInfo_fails() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE,
+                    /* vendorInfo= */ null);
+        });
+
+        assertWithMessage("Exception for null vendor info in announcement constructor")
+                .that(thrown).hasMessageThat().contains("Vendor info cannot be null");
+    }
+
+    @Test
+    public void getSelector() {
+        assertWithMessage("Radio announcement selector")
+                .that(TEST_ANNOUNCEMENT.getSelector()).isEqualTo(FM_PROGRAM_SELECTOR);
+    }
+
+    @Test
+    public void getType() {
+        assertWithMessage("Radio announcement type")
+                .that(TEST_ANNOUNCEMENT.getType()).isEqualTo(TRAFFIC_ANNOUNCEMENT_TYPE);
+    }
+
+    @Test
+    public void getVendorInfo() {
+        assertWithMessage("Radio announcement vendor info")
+                .that(TEST_ANNOUNCEMENT.getVendorInfo()).isEqualTo(VENDOR_INFO);
+    }
+
+    private static Map<String, String> createVendorInfo() {
+        Map<String, String> vendorInfo = new ArrayMap<>();
+        vendorInfo.put("vendorKeyMock", "vendorValueMock");
+        return vendorInfo;
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
new file mode 100644
index 0000000..9bfa2fb
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
@@ -0,0 +1,648 @@
+/*
+ * 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.hardware.radio.tests.unittests;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public final class RadioManagerTest {
+
+    private static final int REGION = RadioManager.REGION_ITU_2;
+    private static final int FM_LOWER_LIMIT = 87500;
+    private static final int FM_UPPER_LIMIT = 108000;
+    private static final int FM_SPACING = 200;
+    private static final int AM_LOWER_LIMIT = 540;
+    private static final int AM_UPPER_LIMIT = 1700;
+    private static final int AM_SPACING = 10;
+    private static final boolean STEREO_SUPPORTED = true;
+    private static final boolean RDS_SUPPORTED = true;
+    private static final boolean TA_SUPPORTED = false;
+    private static final boolean AF_SUPPORTED = false;
+    private static final boolean EA_SUPPORTED = false;
+
+    private static final int PROPERTIES_ID = 10;
+    private static final String SERVICE_NAME = "ServiceNameMock";
+    private static final int CLASS_ID = RadioManager.CLASS_AM_FM;
+    private static final String IMPLEMENTOR = "ImplementorMock";
+    private static final String PRODUCT = "ProductMock";
+    private static final String VERSION = "VersionMock";
+    private static final String SERIAL = "SerialMock";
+    private static final int NUM_TUNERS = 1;
+    private static final int NUM_AUDIO_SOURCES = 1;
+    private static final boolean IS_INITIALIZATION_REQUIRED = false;
+    private static final boolean IS_CAPTURE_SUPPORTED = false;
+    private static final boolean IS_BG_SCAN_SUPPORTED = true;
+    private static final int[] SUPPORTED_PROGRAM_TYPES = new int[]{
+            ProgramSelector.PROGRAM_TYPE_AM, ProgramSelector.PROGRAM_TYPE_FM};
+    private static final int[] SUPPORTED_IDENTIFIERS_TYPES = new int[]{
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI};
+
+    private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR =
+            createFmBandDescriptor();
+    private static final RadioManager.AmBandDescriptor AM_BAND_DESCRIPTOR =
+            createAmBandDescriptor();
+    private static final RadioManager.FmBandConfig FM_BAND_CONFIG = createFmBandConfig();
+    private static final RadioManager.AmBandConfig AM_BAND_CONFIG = createAmBandConfig();
+    private static final RadioManager.ModuleProperties AMFM_PROPERTIES = createAmFmProperties();
+
+    /**
+     * Info flags with live, tuned and stereo enabled
+     */
+    private static final int INFO_FLAGS = 0b110001;
+    private static final int SIGNAL_QUALITY = 2;
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x10000111);
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_RELATED =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x10000113);
+    private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1013);
+    private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 95500);
+    private static final ProgramSelector DAB_SELECTOR =
+            new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER,
+                    new ProgramSelector.Identifier[]{
+                            DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER},
+                    /* vendorIds= */ null);
+    private static final RadioMetadata METADATA = createMetadata();
+    private static final RadioManager.ProgramInfo DAB_PROGRAM_INFO =
+            createDabProgramInfo(DAB_SELECTOR);
+
+    @Test
+    public void getType_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor type")
+                .that(bandDescriptor.getType()).isEqualTo(RadioManager.BAND_AM);
+    }
+
+    @Test
+    public void getRegion_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor();
+
+        assertWithMessage("FM Band Descriptor region")
+                .that(bandDescriptor.getRegion()).isEqualTo(REGION);
+    }
+
+    @Test
+    public void getLowerLimit_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor();
+
+        assertWithMessage("FM Band Descriptor lower limit")
+                .that(bandDescriptor.getLowerLimit()).isEqualTo(FM_LOWER_LIMIT);
+    }
+
+    @Test
+    public void getUpperLimit_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor upper limit")
+                .that(bandDescriptor.getUpperLimit()).isEqualTo(AM_UPPER_LIMIT);
+    }
+
+    @Test
+    public void getSpacing_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor spacing")
+                .that(bandDescriptor.getSpacing()).isEqualTo(AM_SPACING);
+    }
+
+    @Test
+    public void isAmBand_forAmBandDescriptor_returnsTrue() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("Is AM Band Descriptor an AM band")
+                .that(bandDescriptor.isAmBand()).isTrue();
+    }
+
+    @Test
+    public void isFmBand_forAmBandDescriptor_returnsFalse() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("Is AM Band Descriptor an FM band")
+                .that(bandDescriptor.isFmBand()).isFalse();
+    }
+
+    @Test
+    public void isStereoSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor stereo")
+                .that(FM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void isRdsSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor RDS or RBDS")
+                .that(FM_BAND_DESCRIPTOR.isRdsSupported()).isEqualTo(RDS_SUPPORTED);
+    }
+
+    @Test
+    public void isTaSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor traffic announcement")
+                .that(FM_BAND_DESCRIPTOR.isTaSupported()).isEqualTo(TA_SUPPORTED);
+    }
+
+    @Test
+    public void isAfSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor alternate frequency")
+                .that(FM_BAND_DESCRIPTOR.isAfSupported()).isEqualTo(AF_SUPPORTED);
+    }
+
+    @Test
+    public void isEaSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor emergency announcement")
+                .that(FM_BAND_DESCRIPTOR.isEaSupported()).isEqualTo(EA_SUPPORTED);
+    }
+
+    @Test
+    public void isStereoSupported_forAmBandDescriptor() {
+        assertWithMessage("AM Band Descriptor stereo")
+                .that(AM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void equals_withSameFmBandDescriptors_returnsTrue() {
+        RadioManager.FmBandDescriptor fmBandDescriptor1 = createFmBandDescriptor();
+        RadioManager.FmBandDescriptor fmBandDescriptor2 = createFmBandDescriptor();
+
+        assertWithMessage("The same FM Band Descriptor")
+                .that(fmBandDescriptor1).isEqualTo(fmBandDescriptor2);
+    }
+
+    @Test
+    public void equals_withSameAmBandDescriptors_returnsTrue() {
+        RadioManager.AmBandDescriptor amBandDescriptorCompared = createAmBandDescriptor();
+
+        assertWithMessage("The same AM Band Descriptor")
+                .that(AM_BAND_DESCRIPTOR).isEqualTo(amBandDescriptorCompared);
+    }
+
+    @Test
+    public void equals_withAmBandDescriptorsOfDifferentUpperLimits_returnsFalse() {
+        RadioManager.AmBandDescriptor amBandDescriptorCompared =
+                new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+                        AM_UPPER_LIMIT + AM_SPACING, AM_SPACING, STEREO_SUPPORTED);
+
+        assertWithMessage("AM Band Descriptor of different upper limit")
+                .that(AM_BAND_DESCRIPTOR).isNotEqualTo(amBandDescriptorCompared);
+    }
+
+    @Test
+    public void equals_withAmAndFmBandDescriptors_returnsFalse() {
+        assertWithMessage("AM Band Descriptor")
+                .that(AM_BAND_DESCRIPTOR).isNotEqualTo(FM_BAND_DESCRIPTOR);
+    }
+
+    @Test
+    public void getType_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config type")
+                .that(fmBandConfig.getType()).isEqualTo(RadioManager.BAND_FM);
+    }
+
+    @Test
+    public void getRegion_forBandConfig() {
+        RadioManager.BandConfig amBandConfig = createAmBandConfig();
+
+        assertWithMessage("AM Band Config region")
+                .that(amBandConfig.getRegion()).isEqualTo(REGION);
+    }
+
+    @Test
+    public void getLowerLimit_forBandConfig() {
+        RadioManager.BandConfig amBandConfig = createAmBandConfig();
+
+        assertWithMessage("AM Band Config lower limit")
+                .that(amBandConfig.getLowerLimit()).isEqualTo(AM_LOWER_LIMIT);
+    }
+
+    @Test
+    public void getUpperLimit_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config upper limit")
+                .that(fmBandConfig.getUpperLimit()).isEqualTo(FM_UPPER_LIMIT);
+    }
+
+    @Test
+    public void getSpacing_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config spacing")
+                .that(fmBandConfig.getSpacing()).isEqualTo(FM_SPACING);
+    }
+
+    @Test
+    public void getStereo_forFmBandConfig() {
+        assertWithMessage("FM Band Config stereo ")
+                .that(FM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void getRds_forFmBandConfig() {
+        assertWithMessage("FM Band Config RDS or RBDS")
+                .that(FM_BAND_CONFIG.getRds()).isEqualTo(RDS_SUPPORTED);
+    }
+
+    @Test
+    public void getTa_forFmBandConfig() {
+        assertWithMessage("FM Band Config traffic announcement")
+                .that(FM_BAND_CONFIG.getTa()).isEqualTo(TA_SUPPORTED);
+    }
+
+    @Test
+    public void getAf_forFmBandConfig() {
+        assertWithMessage("FM Band Config alternate frequency")
+                .that(FM_BAND_CONFIG.getAf()).isEqualTo(AF_SUPPORTED);
+    }
+
+    @Test
+    public void getEa_forFmBandConfig() {
+        assertWithMessage("FM Band Config emergency Announcement")
+                .that(FM_BAND_CONFIG.getEa()).isEqualTo(EA_SUPPORTED);
+    }
+
+    @Test
+    public void getStereo_forAmBandConfig() {
+        assertWithMessage("AM Band Config stereo")
+                .that(AM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void equals_withSameFmBandConfigs_returnsTrue() {
+        RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig();
+
+        assertWithMessage("The same FM Band Config")
+                .that(FM_BAND_CONFIG).isEqualTo(fmBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withFmBandConfigsOfDifferentAfs_returnsFalse() {
+        RadioManager.FmBandConfig.Builder builder = new RadioManager.FmBandConfig.Builder(
+                createFmBandDescriptor()).setStereo(STEREO_SUPPORTED).setRds(RDS_SUPPORTED)
+                .setTa(TA_SUPPORTED).setAf(!AF_SUPPORTED).setEa(EA_SUPPORTED);
+        RadioManager.FmBandConfig fmBandConfigFromBuilder = builder.build();
+
+        assertWithMessage("FM Band Config of different af value")
+                .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigFromBuilder);
+    }
+
+    @Test
+    public void equals_withFmAndAmBandConfigs_returnsFalse() {
+        assertWithMessage("FM Band Config")
+                .that(FM_BAND_CONFIG).isNotEqualTo(AM_BAND_CONFIG);
+    }
+
+    @Test
+    public void equals_withSameAmBandConfigs_returnsTrue() {
+        RadioManager.AmBandConfig amBandConfigCompared = createAmBandConfig();
+
+        assertWithMessage("The same AM Band Config")
+                .that(AM_BAND_CONFIG).isEqualTo(amBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withAmBandConfigsOfDifferentTypes_returnsFalse() {
+        RadioManager.AmBandConfig amBandConfigCompared = new RadioManager.AmBandConfig(
+                new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM_HD, AM_LOWER_LIMIT,
+                        AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED));
+
+        assertWithMessage("AM Band Config of different type")
+                .that(AM_BAND_CONFIG).isNotEqualTo(amBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withAmBandConfigsOfDifferentStereoValues_returnsFalse() {
+        RadioManager.AmBandConfig.Builder builder = new RadioManager.AmBandConfig.Builder(
+                createAmBandDescriptor()).setStereo(!STEREO_SUPPORTED);
+        RadioManager.AmBandConfig amBandConfigFromBuilder = builder.build();
+
+        assertWithMessage("AM Band Config of different stereo value")
+                .that(AM_BAND_CONFIG).isNotEqualTo(amBandConfigFromBuilder);
+    }
+
+    @Test
+    public void getId_forModuleProperties() {
+        assertWithMessage("Properties id")
+                .that(AMFM_PROPERTIES.getId()).isEqualTo(PROPERTIES_ID);
+    }
+
+    @Test
+    public void getServiceName_forModuleProperties() {
+        assertWithMessage("Properties service name")
+                .that(AMFM_PROPERTIES.getServiceName()).isEqualTo(SERVICE_NAME);
+    }
+
+    @Test
+    public void getClassId_forModuleProperties() {
+        assertWithMessage("Properties class ID")
+                .that(AMFM_PROPERTIES.getClassId()).isEqualTo(CLASS_ID);
+    }
+
+    @Test
+    public void getImplementor_forModuleProperties() {
+        assertWithMessage("Properties implementor")
+                .that(AMFM_PROPERTIES.getImplementor()).isEqualTo(IMPLEMENTOR);
+    }
+
+    @Test
+    public void getProduct_forModuleProperties() {
+        assertWithMessage("Properties product")
+                .that(AMFM_PROPERTIES.getProduct()).isEqualTo(PRODUCT);
+    }
+
+    @Test
+    public void getVersion_forModuleProperties() {
+        assertWithMessage("Properties version")
+                .that(AMFM_PROPERTIES.getVersion()).isEqualTo(VERSION);
+    }
+
+    @Test
+    public void getSerial_forModuleProperties() {
+        assertWithMessage("Serial properties")
+                .that(AMFM_PROPERTIES.getSerial()).isEqualTo(SERIAL);
+    }
+
+    @Test
+    public void getNumTuners_forModuleProperties() {
+        assertWithMessage("Number of tuners in properties")
+                .that(AMFM_PROPERTIES.getNumTuners()).isEqualTo(NUM_TUNERS);
+    }
+
+    @Test
+    public void getNumAudioSources_forModuleProperties() {
+        assertWithMessage("Number of audio sources in properties")
+                .that(AMFM_PROPERTIES.getNumAudioSources()).isEqualTo(NUM_AUDIO_SOURCES);
+    }
+
+    @Test
+    public void isInitializationRequired_forModuleProperties() {
+        assertWithMessage("Initialization required in properties")
+                .that(AMFM_PROPERTIES.isInitializationRequired())
+                .isEqualTo(IS_INITIALIZATION_REQUIRED);
+    }
+
+    @Test
+    public void isCaptureSupported_forModuleProperties() {
+        assertWithMessage("Capture support in properties")
+                .that(AMFM_PROPERTIES.isCaptureSupported()).isEqualTo(IS_CAPTURE_SUPPORTED);
+    }
+
+    @Test
+    public void isBackgroundScanningSupported_forModuleProperties() {
+        assertWithMessage("Background scan support in properties")
+                .that(AMFM_PROPERTIES.isBackgroundScanningSupported())
+                .isEqualTo(IS_BG_SCAN_SUPPORTED);
+    }
+
+    @Test
+    public void isProgramTypeSupported_withSupportedType_forModuleProperties() {
+        assertWithMessage("AM/FM frequency type radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramTypeSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY))
+                .isTrue();
+    }
+
+    @Test
+    public void isProgramTypeSupported_withNonSupportedType_forModuleProperties() {
+        assertWithMessage("DAB frequency type radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramTypeSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY)).isFalse();
+    }
+
+    @Test
+    public void isProgramIdentifierSupported_withSupportedIdentifier_forModuleProperties() {
+        assertWithMessage("AM/FM frequency identifier radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramIdentifierSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)).isTrue();
+    }
+
+    @Test
+    public void isProgramIdentifierSupported_withNonSupportedIdentifier_forModuleProperties() {
+        assertWithMessage("DAB frequency identifier radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramIdentifierSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY)).isFalse();
+    }
+
+    @Test
+    public void getDabFrequencyTable_forModuleProperties() {
+        assertWithMessage("Properties DAB frequency table")
+                .that(AMFM_PROPERTIES.getDabFrequencyTable()).isNull();
+    }
+
+    @Test
+    public void getVendorInfo_forModuleProperties() {
+        assertWithMessage("Properties vendor info")
+                .that(AMFM_PROPERTIES.getVendorInfo()).isEmpty();
+    }
+
+    @Test
+    public void getBands_forModuleProperties() {
+        assertWithMessage("Properties bands")
+                .that(AMFM_PROPERTIES.getBands()).asList()
+                .containsExactly(AM_BAND_DESCRIPTOR, FM_BAND_DESCRIPTOR);
+    }
+
+    @Test
+    public void equals_withSameProperties_returnsTrue() {
+        RadioManager.ModuleProperties propertiesCompared = createAmFmProperties();
+
+        assertWithMessage("The same module properties")
+                .that(AMFM_PROPERTIES).isEqualTo(propertiesCompared);
+    }
+
+    @Test
+    public void equals_withModulePropertiesOfDifferentIds_returnsFalse() {
+        RadioManager.ModuleProperties propertiesDab = new RadioManager.ModuleProperties(
+                PROPERTIES_ID + 1, SERVICE_NAME, CLASS_ID, IMPLEMENTOR, PRODUCT, VERSION,
+                SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES, IS_INITIALIZATION_REQUIRED,
+                IS_CAPTURE_SUPPORTED, /* bands= */ null, IS_BG_SCAN_SUPPORTED,
+                SUPPORTED_PROGRAM_TYPES, SUPPORTED_IDENTIFIERS_TYPES, /* dabFrequencyTable= */ null,
+                /* vendorInfo= */ null);
+
+        assertWithMessage("Module properties of different id")
+                .that(AMFM_PROPERTIES).isNotEqualTo(propertiesDab);
+    }
+
+    @Test
+    public void getSelector_forProgramInfo() {
+        assertWithMessage("Selector of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSelector()).isEqualTo(DAB_SELECTOR);
+    }
+
+    @Test
+    public void getLogicallyTunedTo_forProgramInfo() {
+        assertWithMessage("Identifier logically tuned to in DAB program info")
+                .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER);
+    }
+
+    @Test
+    public void getPhysicallyTunedTo_forProgramInfo() {
+        assertWithMessage("Identifier physically tuned to DAB program info")
+                .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER);
+    }
+
+    @Test
+    public void getRelatedContent_forProgramInfo() {
+        assertWithMessage("Related contents of DAB program info")
+                .that(DAB_PROGRAM_INFO.getRelatedContent())
+                .containsExactly(DAB_SID_EXT_IDENTIFIER_RELATED);
+    }
+
+    @Test
+    public void getChannel_forProgramInfo() {
+        assertWithMessage("Main channel of DAB program info")
+                .that(DAB_PROGRAM_INFO.getChannel()).isEqualTo(0);
+    }
+
+    @Test
+    public void getSubChannel_forProgramInfo() {
+        assertWithMessage("Sub channel of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSubChannel()).isEqualTo(0);
+    }
+
+    @Test
+    public void isTuned_forProgramInfo() {
+        assertWithMessage("Tuned status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isTuned()).isTrue();
+    }
+
+    @Test
+    public void isStereo_forProgramInfo() {
+        assertWithMessage("Stereo support in DAB program info")
+                .that(DAB_PROGRAM_INFO.isStereo()).isTrue();
+    }
+
+    @Test
+    public void isDigital_forProgramInfo() {
+        assertWithMessage("Digital DAB program info")
+                .that(DAB_PROGRAM_INFO.isDigital()).isTrue();
+    }
+
+    @Test
+    public void isLive_forProgramInfo() {
+        assertWithMessage("Live status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isLive()).isTrue();
+    }
+
+    @Test
+    public void isMuted_forProgramInfo() {
+        assertWithMessage("Muted status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isMuted()).isFalse();
+    }
+
+    @Test
+    public void isTrafficProgram_forProgramInfo() {
+        assertWithMessage("Traffic program support in DAB program info")
+                .that(DAB_PROGRAM_INFO.isTrafficProgram()).isFalse();
+    }
+
+    @Test
+    public void isTrafficAnnouncementActive_forProgramInfo() {
+        assertWithMessage("Active traffic announcement for DAB program info")
+                .that(DAB_PROGRAM_INFO.isTrafficAnnouncementActive()).isFalse();
+    }
+
+    @Test
+    public void getSignalStrength_forProgramInfo() {
+        assertWithMessage("Signal strength of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSignalStrength()).isEqualTo(SIGNAL_QUALITY);
+    }
+
+    @Test
+    public void getMetadata_forProgramInfo() {
+        assertWithMessage("Metadata of DAB program info")
+                .that(DAB_PROGRAM_INFO.getMetadata()).isEqualTo(METADATA);
+    }
+
+    @Test
+    public void getVendorInfo_forProgramInfo() {
+        assertWithMessage("Vendor info of DAB program info")
+                .that(DAB_PROGRAM_INFO.getVendorInfo()).isEmpty();
+    }
+
+    @Test
+    public void equals_withSameProgramInfo_returnsTrue() {
+        RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(DAB_SELECTOR);
+
+        assertWithMessage("The same program info")
+                .that(dabProgramInfoCompared).isEqualTo(DAB_PROGRAM_INFO);
+    }
+
+    @Test
+    public void equals_withSameProgramInfoOfDifferentSecondaryIdSelectors_returnsFalse() {
+        ProgramSelector dabSelectorCompared = new ProgramSelector(
+                ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER,
+                new ProgramSelector.Identifier[]{DAB_FREQUENCY_IDENTIFIER},
+                /* vendorIds= */ null);
+        RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(dabSelectorCompared);
+
+        assertWithMessage("Program info with different secondary id selectors")
+                .that(DAB_PROGRAM_INFO).isNotEqualTo(dabProgramInfoCompared);
+    }
+
+    private static RadioManager.ModuleProperties createAmFmProperties() {
+        return new RadioManager.ModuleProperties(PROPERTIES_ID, SERVICE_NAME, CLASS_ID,
+                IMPLEMENTOR, PRODUCT, VERSION, SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES,
+                IS_INITIALIZATION_REQUIRED, IS_CAPTURE_SUPPORTED,
+                new RadioManager.BandDescriptor[]{AM_BAND_DESCRIPTOR, FM_BAND_DESCRIPTOR},
+                IS_BG_SCAN_SUPPORTED, SUPPORTED_PROGRAM_TYPES, SUPPORTED_IDENTIFIERS_TYPES,
+                /* dabFrequencyTable= */ null, /* vendorInfo= */ null);
+    }
+
+    private static RadioManager.FmBandDescriptor createFmBandDescriptor() {
+        return new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT,
+                FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED,
+                AF_SUPPORTED, EA_SUPPORTED);
+    }
+
+    private static RadioManager.AmBandDescriptor createAmBandDescriptor() {
+        return new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+                AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED);
+    }
+
+    private static RadioManager.FmBandConfig createFmBandConfig() {
+        return new RadioManager.FmBandConfig(createFmBandDescriptor());
+    }
+
+    private static RadioManager.AmBandConfig createAmBandConfig() {
+        return new RadioManager.AmBandConfig(createAmBandDescriptor());
+    }
+
+    private static RadioMetadata createMetadata() {
+        RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder();
+        return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest").build();
+    }
+
+    private static RadioManager.ProgramInfo createDabProgramInfo(ProgramSelector selector) {
+        return new RadioManager.ProgramInfo(selector, DAB_FREQUENCY_IDENTIFIER,
+                DAB_SID_EXT_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS,
+                SIGNAL_QUALITY, METADATA, /* vendorInfo= */ null);
+    }
+
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
new file mode 100644
index 0000000..7f4ea11
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
@@ -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.server.broadcastradio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+
+import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link android.hardware.radio.IRadioService} with AIDL HAL implementation
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class IRadioServiceAidlImplTest {
+
+    private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private IRadioServiceAidlImpl mAidlImpl;
+
+    @Mock
+    private BroadcastRadioService mServiceMock;
+    @Mock
+    private BroadcastRadioServiceImpl mHalMock;
+    @Mock
+    private RadioManager.ModuleProperties mModuleMock;
+    @Mock
+    private RadioManager.BandConfig mBandConfigMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private ICloseHandle mICloseHandle;
+    @Mock
+    private ITuner mTunerMock;
+
+    @Before
+    public void setUp() throws Exception {
+        doNothing().when(mServiceMock).enforcePolicyAccess();
+
+        when(mHalMock.listModules()).thenReturn(Arrays.asList(mModuleMock));
+        when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any()))
+                .thenReturn(mTunerMock);
+        when(mHalMock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
+
+        mAidlImpl = new IRadioServiceAidlImpl(mServiceMock, mHalMock);
+    }
+
+    @Test
+    public void loadModules_forAidlImpl() {
+        assertWithMessage("Modules loaded in AIDL HAL")
+                .that(mAidlImpl.listModules())
+                .containsExactly(mModuleMock);
+    }
+
+    @Test
+    public void openTuner_forAidlImpl() throws Exception {
+        ITuner tuner = mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in AIDL HAL")
+                .that(tuner).isEqualTo(mTunerMock);
+    }
+
+    @Test
+    public void addAnnouncementListener_forAidlImpl() {
+        ICloseHandle closeHandle = mAidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+
+        verify(mHalMock).addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+        assertWithMessage("Close handle of announcement listener for HAL 2")
+                .that(closeHandle).isEqualTo(mICloseHandle);
+    }
+
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
new file mode 100644
index 0000000..f28e27d
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.broadcastradio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link android.hardware.radio.IRadioService} with HIDL HAL implementation
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class IRadioServiceHidlImplTest {
+
+    private static final int HAL1_MODULE_ID = 0;
+    private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private IRadioServiceHidlImpl mHidlImpl;
+
+    @Mock
+    private BroadcastRadioService mServiceMock;
+    @Mock
+    private com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1Mock;
+    @Mock
+    private com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2Mock;
+    @Mock
+    private RadioManager.ModuleProperties mHal1ModuleMock;
+    @Mock
+    private RadioManager.ModuleProperties mHal2ModuleMock;
+    @Mock
+    private RadioManager.BandConfig mBandConfigMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private ICloseHandle mICloseHandle;
+    @Mock
+    private ITuner mHal1TunerMock;
+    @Mock
+    private ITuner mHal2TunerMock;
+
+    @Before
+    public void setup() throws Exception {
+        doNothing().when(mServiceMock).enforcePolicyAccess();
+        when(mHal1Mock.loadModules()).thenReturn(Arrays.asList(mHal1ModuleMock));
+        when(mHal1Mock.openTuner(anyInt(), any(), anyBoolean(), any())).thenReturn(mHal1TunerMock);
+
+        when(mHal2Mock.listModules()).thenReturn(Arrays.asList(mHal2ModuleMock));
+        doAnswer(invocation -> {
+            int moduleId = (int) invocation.getArguments()[0];
+            return moduleId != HAL1_MODULE_ID;
+        }).when(mHal2Mock).hasModule(anyInt());
+        when(mHal2Mock.openSession(anyInt(), any(), anyBoolean(), any()))
+                .thenReturn(mHal2TunerMock);
+        when(mHal2Mock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
+
+        mHidlImpl = new IRadioServiceHidlImpl(mServiceMock, mHal1Mock, mHal2Mock);
+    }
+
+    @Test
+    public void loadModules_forHidlImpl() {
+        assertWithMessage("Modules loaded in HIDL HAL")
+                .that(mHidlImpl.listModules())
+                .containsExactly(mHal1ModuleMock, mHal2ModuleMock);
+    }
+
+    @Test
+    public void openTuner_withHal1ModuleId_forHidlImpl() throws Exception {
+        ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in HAL 1")
+                .that(tuner).isEqualTo(mHal1TunerMock);
+    }
+
+    @Test
+    public void openTuner_withHal2ModuleId_forHidlImpl() throws Exception {
+        ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID + 1, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in HAL 2")
+                .that(tuner).isEqualTo(mHal2TunerMock);
+    }
+
+    @Test
+    public void addAnnouncementListener_forHidlImpl() {
+        when(mHal2Mock.hasAnyModules()).thenReturn(true);
+        ICloseHandle closeHandle = mHidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+
+        verify(mHal2Mock).addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+        assertWithMessage("Close handle of announcement listener for HAL 2")
+                .that(closeHandle).isEqualTo(mICloseHandle);
+    }
+
+}
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 0b8b29b..bcb13d2 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -48,6 +48,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.fail;
 
 import static org.junit.Assert.assertEquals;
@@ -56,7 +57,9 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
 import android.app.Notification.CallStyle;
@@ -68,6 +71,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
+import android.graphics.Typeface;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
@@ -79,7 +83,9 @@
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
 import android.text.style.TextAppearanceSpan;
+import android.util.Pair;
 import android.widget.RemoteViews;
 
 import androidx.test.InstrumentationRegistry;
@@ -89,6 +95,8 @@
 import com.android.internal.R;
 import com.android.internal.util.ContrastColorUtil;
 
+import junit.framework.Assert;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -218,8 +226,10 @@
 
     @Test
     public void allPendingIntents_recollectedAfterReusingBuilder() {
-        PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), PendingIntent.FLAG_MUTABLE_UNAUDITED);
-        PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        PendingIntent intent2 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test2"), PendingIntent.FLAG_IMMUTABLE);
 
         Notification.Builder builder = new Notification.Builder(mContext, "channel");
         builder.setContentIntent(intent1);
@@ -669,30 +679,23 @@
         Notification notification = new Notification.Builder(mContext, "Channel").setStyle(
                 style).build();
 
+        int targetSize = mContext.getResources().getDimensionPixelSize(
+                ActivityManager.isLowRamDeviceStatic()
+                        ? R.dimen.notification_person_icon_max_size_low_ram
+                        : R.dimen.notification_person_icon_max_size);
+
         Bitmap personIcon = style.getUser().getIcon().getBitmap();
-        assertThat(personIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(personIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(personIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(personIcon.getHeight()).isEqualTo(targetSize);
 
         Bitmap avatarIcon = style.getMessages().get(0).getSenderPerson().getIcon().getBitmap();
-        assertThat(avatarIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(avatarIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(avatarIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(avatarIcon.getHeight()).isEqualTo(targetSize);
 
         Bitmap historicAvatarIcon = style.getHistoricMessages().get(
                 0).getSenderPerson().getIcon().getBitmap();
-        assertThat(historicAvatarIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(historicAvatarIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(historicAvatarIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(historicAvatarIcon.getHeight()).isEqualTo(targetSize);
     }
 
     @Test
@@ -780,7 +783,6 @@
         assertFalse(notification.isMediaNotification());
     }
 
-    @Test
     public void validateColorizedPaletteForColor(int rawColor) {
         Notification.Colors cDay = new Notification.Colors();
         Notification.Colors cNight = new Notification.Colors();
@@ -861,19 +863,22 @@
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_LARGE_ICON_BIG, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
 
         // no crash, good
     }
 
     @Test
     public void testRestoreFromExtras_Messaging_invalidExtra_noCrash() {
-        Notification.Style style = new Notification.MessagingStyle();
+        Notification.Style style = new Notification.MessagingStyle("test");
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_MESSAGING_PERSON, new Bundle());
         fakeTypes.putParcelable(EXTRA_CONVERSATION_ICON, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
 
         // no crash, good
     }
@@ -885,22 +890,33 @@
         fakeTypes.putParcelable(EXTRA_MEDIA_SESSION, new Bundle());
         fakeTypes.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
 
         // no crash, good
     }
 
     @Test
     public void testRestoreFromExtras_Call_invalidExtra_noCrash() {
-        Notification.Style style = new CallStyle();
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Notification.Style style = Notification.CallStyle.forIncomingCall(
+                new Person.Builder().setName("hi").build(), intent1, intent1);
+
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_CALL_PERSON, new Bundle());
         fakeTypes.putParcelable(EXTRA_ANSWER_INTENT, new Bundle());
         fakeTypes.putParcelable(EXTRA_DECLINE_INTENT, new Bundle());
         fakeTypes.putParcelable(EXTRA_HANG_UP_INTENT, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
-
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
         // no crash, good
     }
 
@@ -962,7 +978,11 @@
         fakeTypes.putParcelable(KEY_ON_READ, new Bundle());
         fakeTypes.putParcelable(KEY_ON_REPLY, new Bundle());
         fakeTypes.putParcelable(KEY_REMOTE_INPUT, new Bundle());
-        Notification.CarExtender.UnreadConversation.getUnreadConversationFromBundle(fakeTypes);
+
+        Notification n = new Notification.Builder(mContext, "test")
+                .setExtras(fakeTypes)
+                .build();
+        Notification.CarExtender extender = new Notification.CarExtender(n);
 
         // no crash, good
     }
@@ -980,6 +1000,493 @@
         // no crash, good
     }
 
+
+    @Test
+    public void testDoesNotStripsExtenders() {
+        Notification.Builder nb = new Notification.Builder(mContext, "channel");
+        nb.extend(new Notification.CarExtender().setColor(Color.RED));
+        nb.extend(new Notification.TvExtender().setChannelId("different channel"));
+        nb.extend(new Notification.WearableExtender().setDismissalId("dismiss"));
+        Notification before = nb.build();
+        Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before);
+
+        assertTrue(before == after);
+
+        Assert.assertEquals("different channel",
+                new Notification.TvExtender(before).getChannelId());
+        Assert.assertEquals(Color.RED, new Notification.CarExtender(before).getColor());
+        Assert.assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId());
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_noStyles() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test");
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+
+        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_noStyleToStyle() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test");
+        Notification.Builder n2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_styleToNoStyle() {
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_changeStyle() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle());
+        Notification.Builder n2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testInboxTextChange() {
+        Notification.Builder nInbox1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle().addLine("a").addLine("b"));
+        Notification.Builder nInbox2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle().addLine("b").addLine("c"));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2));
+    }
+
+    @Test
+    public void testBigTextTextChange() {
+        Notification.Builder nBigText1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle().bigText("something"));
+        Notification.Builder nBigText2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle().bigText("else"));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2));
+    }
+
+    @Test
+    public void testBigPictureChange() {
+        Bitmap bitA = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+        Bitmap bitB = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
+
+        Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigPictureStyle().bigPicture(bitA));
+        Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigPictureStyle().bigPicture(bitB));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2));
+    }
+
+    @Test
+    public void testMessagingChange_text() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build()))
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "b", 100, new Person.Builder().setName("hi").build()))
+                );
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_data() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())
+                                .setData("text", mock(Uri.class))));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_sender() {
+        Person a = new Person.Builder().setName("A").build();
+        Person b = new Person.Builder().setName("b").build();
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_key() {
+        Person a = new Person.Builder().setName("hi").setKey("A").build();
+        Person b = new Person.Builder().setName("hi").setKey("b").build();
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_ignoreTimeChange() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 1000, new Person.Builder().setName("hi").build()))
+                );
+
+        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testRemoteViews_nullChange() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setContent(mock(RemoteViews.class));
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test")
+                .setContent(mock(RemoteViews.class));
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test")
+                .setCustomBigContentView(mock(RemoteViews.class));
+        n2 = new Notification.Builder(mContext, "test");
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test")
+                .setCustomBigContentView(mock(RemoteViews.class));
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test");
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_layoutChange() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(189);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_layoutSame() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_sequenceChange() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        when(a.getSequenceNumber()).thenReturn(1);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+        when(b.getSequenceNumber()).thenReturn(2);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_sequenceSame() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        when(a.getSequenceNumber()).thenReturn(1);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+        when(b.getSequenceNumber()).thenReturn(1);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferent_null() {
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentSame() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentText() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
+                .build();
+
+        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentSpannables() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon,
+                        new SpannableStringBuilder().append("test1",
+                                new StyleSpan(Typeface.BOLD),
+                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE),
+                        intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "test1", intent).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentNumber() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
+                .build();
+
+        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentIntent() {
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        PendingIntent intent2 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsIgnoresRemoteInputs() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(new RemoteInput.Builder("a")
+                                .setChoices(new CharSequence[] {"i", "m"})
+                                .build())
+                        .build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(new RemoteInput.Builder("a")
+                                .setChoices(new CharSequence[] {"t", "m"})
+                                .build())
+                        .build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_noRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .build())
+                .build();
+        Assert.assertNull(notification.findRemoteInputActionPair(false));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_hasRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        RemoteInput remoteInput = new RemoteInput.Builder("a").build();
+
+        Notification.Action actionWithRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(remoteInput)
+                        .addRemoteInput(remoteInput)
+                        .build();
+
+        Notification.Action actionWithoutRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 2", intent)
+                        .build();
+
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(actionWithoutRemoteInput)
+                .addAction(actionWithRemoteInput)
+                .build();
+
+        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
+                notification.findRemoteInputActionPair(false);
+
+        assertNotNull(remoteInputActionPair);
+        Assert.assertEquals(remoteInput, remoteInputActionPair.first);
+        Assert.assertEquals(actionWithRemoteInput, remoteInputActionPair.second);
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(
+                                new RemoteInput.Builder("a")
+                                        .setAllowFreeFormInput(false).build())
+                        .build())
+                .build();
+        Assert.assertNull(notification.findRemoteInputActionPair(true));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        RemoteInput remoteInput =
+                new RemoteInput.Builder("a").setAllowFreeFormInput(false).build();
+        RemoteInput freeformRemoteInput =
+                new RemoteInput.Builder("b").setAllowFreeFormInput(true).build();
+
+        Notification.Action actionWithFreeformRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(remoteInput)
+                        .addRemoteInput(freeformRemoteInput)
+                        .build();
+
+        Notification.Action actionWithoutFreeformRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 2", intent)
+                        .addRemoteInput(remoteInput)
+                        .build();
+
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(actionWithoutFreeformRemoteInput)
+                .addAction(actionWithFreeformRemoteInput)
+                .build();
+
+        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
+                notification.findRemoteInputActionPair(true);
+
+        assertNotNull(remoteInputActionPair);
+        Assert.assertEquals(freeformRemoteInput, remoteInputActionPair.first);
+        Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second);
+    }
+
     private void assertValid(Notification.Colors c) {
         // Assert that all colors are populated
         assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
diff --git a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java b/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java
deleted file mode 100644
index 7c7cd12..0000000
--- a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app.time;
-
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class TimeConfigurationTest {
-
-    @Test
-    public void testBuilder() {
-        TimeConfiguration first = new TimeConfiguration.Builder()
-                .setAutoDetectionEnabled(true)
-                .build();
-
-        assertThat(first.isAutoDetectionEnabled()).isTrue();
-
-        TimeConfiguration copyFromBuilderConfiguration = new TimeConfiguration.Builder(first)
-                .build();
-
-        assertThat(first).isEqualTo(copyFromBuilderConfiguration);
-    }
-
-    @Test
-    public void testParcelable() {
-        TimeConfiguration.Builder builder = new TimeConfiguration.Builder();
-
-        assertRoundTripParcelable(builder.setAutoDetectionEnabled(true).build());
-
-        assertRoundTripParcelable(builder.setAutoDetectionEnabled(false).build());
-    }
-
-}
diff --git a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
index 3ab01f3..e7d352c 100644
--- a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
+++ b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
@@ -16,11 +16,9 @@
 
 package android.app.time;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 
 import android.os.ShellCommand;
 
@@ -31,35 +29,12 @@
 
 /**
  * Tests for non-SDK methods on {@link UnixEpochTime}.
+ *
+ * <p>See also {@link android.app.time.cts.UnixEpochTimeTest} for SDK methods.
  */
 @RunWith(AndroidJUnit4.class)
 public class UnixEpochTimeTest {
 
-    @Test
-    public void testEqualsAndHashcode() {
-        UnixEpochTime one1000one = new UnixEpochTime(1000, 1);
-        assertEqualsAndHashCode(one1000one, one1000one);
-
-        UnixEpochTime one1000two = new UnixEpochTime(1000, 1);
-        assertEqualsAndHashCode(one1000one, one1000two);
-
-        UnixEpochTime two1000 = new UnixEpochTime(1000, 2);
-        assertNotEquals(one1000one, two1000);
-
-        UnixEpochTime one2000 = new UnixEpochTime(2000, 1);
-        assertNotEquals(one1000one, one2000);
-    }
-
-    private static void assertEqualsAndHashCode(Object one, Object two) {
-        assertEquals(one, two);
-        assertEquals(one.hashCode(), two.hashCode());
-    }
-
-    @Test
-    public void testParceling() {
-        assertRoundTripParcelable(new UnixEpochTime(1000, 1));
-    }
-
     @Test(expected = IllegalArgumentException.class)
     public void testParseCommandLineArg_noElapsedRealtime() {
         ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
@@ -91,22 +66,6 @@
     }
 
     @Test
-    public void testAt() {
-        long timeMillis = 1000L;
-        int elapsedRealtimeMillis = 100;
-        UnixEpochTime unixEpochTime = new UnixEpochTime(elapsedRealtimeMillis, timeMillis);
-        // Reference time is after the timestamp.
-        UnixEpochTime at125 = unixEpochTime.at(125);
-        assertEquals(timeMillis + (125 - elapsedRealtimeMillis), at125.getUnixEpochTimeMillis());
-        assertEquals(125, at125.getElapsedRealtimeMillis());
-
-        // Reference time is before the timestamp.
-        UnixEpochTime at75 = unixEpochTime.at(75);
-        assertEquals(timeMillis + (75 - elapsedRealtimeMillis), at75.getUnixEpochTimeMillis());
-        assertEquals(75, at75.getElapsedRealtimeMillis());
-    }
-
-    @Test
     public void testElapsedRealtimeDifference() {
         UnixEpochTime value1 = new UnixEpochTime(1000, 123L);
         assertEquals(0, UnixEpochTime.elapsedRealtimeDifference(value1, value1));
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index f448cb3..f370ebd 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -60,6 +60,8 @@
     private OnBackAnimationCallback mCallback1;
     @Mock
     private OnBackAnimationCallback mCallback2;
+    @Mock
+    private BackEvent mBackEvent;
 
     @Before
     public void setUp() throws Exception {
@@ -85,14 +87,14 @@
         verify(mWindowSession, times(2)).setOnBackInvokedCallbackInfo(
                 Mockito.eq(mWindow),
                 captor.capture());
-        captor.getAllValues().get(0).getCallback().onBackStarted();
+        captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted();
+        verify(mCallback1).onBackStarted(mBackEvent);
         verifyZeroInteractions(mCallback2);
 
-        captor.getAllValues().get(1).getCallback().onBackStarted();
+        captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted();
+        verify(mCallback2).onBackStarted(mBackEvent);
         verifyNoMoreInteractions(mCallback1);
     }
 
@@ -110,9 +112,9 @@
                 Mockito.eq(mWindow), captor.capture());
         verifyNoMoreInteractions(mWindowSession);
         assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY);
-        captor.getValue().getCallback().onBackStarted();
+        captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted();
+        verify(mCallback1).onBackStarted(mBackEvent);
     }
 
     @Test
@@ -148,8 +150,8 @@
         mDispatcher.registerOnBackInvokedCallback(
                 OnBackInvokedDispatcher.PRIORITY_OVERLAY, mCallback2);
         verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture());
-        captor.getValue().getCallback().onBackStarted();
+        captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted();
+        verify(mCallback2).onBackStarted(mBackEvent);
     }
 }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 699e794..decfb9f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -265,6 +265,7 @@
         <permission name="android.permission.INSTALL_LOCATION_PROVIDER"/>
         <permission name="android.permission.INSTALL_PACKAGES"/>
         <permission name="android.permission.INSTALL_PACKAGE_UPDATES"/>
+        <permission name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"/>
         <!-- Needed for test only -->
         <permission name="android.permission.ACCESS_MTP"/>
         <!-- Needed for test only -->
diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java
index 1c41d06..9940ca3 100644
--- a/graphics/java/android/graphics/BLASTBufferQueue.java
+++ b/graphics/java/android/graphics/BLASTBufferQueue.java
@@ -47,7 +47,7 @@
             TransactionHangCallback callback);
 
     public interface TransactionHangCallback {
-        void onTransactionHang(boolean isGpuHang);
+        void onTransactionHang(String reason);
     }
 
     /** Create a new connection with the surface flinger. */
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 0bc7085..3ee20ea 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -321,4 +321,21 @@
 
     <!-- The smaller size of the dismiss target (shrinks when something is in the target). -->
     <dimen name="floating_dismiss_circle_small">120dp</dimen>
+
+    <!-- The thickness of shadows of a window that has focus in DIP. -->
+    <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen>
+
+    <!-- The thickness of shadows of a window that doesn't have focus in DIP. -->
+    <dimen name="freeform_decor_shadow_unfocused_thickness">5dp</dimen>
+
+    <!-- Height of button (32dp)  + 2 * margin (5dp each). -->
+    <dimen name="freeform_decor_caption_height">42dp</dimen>
+
+    <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). -->
+    <dimen name="freeform_decor_caption_width">216dp</dimen>
+
+    <dimen name="freeform_resize_handle">30dp</dimen>
+
+    <dimen name="freeform_resize_corner">44dp</dimen>
+
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 43f39b7..db5de43 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -76,10 +76,15 @@
             "persist.wm.debug.predictive_back_progress_threshold";
     public static final boolean IS_ENABLED =
             SystemProperties.getInt("persist.wm.debug.predictive_back",
-                    SETTING_VALUE_ON) != SETTING_VALUE_OFF;
+                    SETTING_VALUE_ON) != SETTING_VALUE_ON;
     private static final int PROGRESS_THRESHOLD = SystemProperties
             .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
-
+     /** Flag for U animation features */
+    public static boolean IS_U_ANIMATION_ENABLED =
+            SystemProperties.getInt("persist.wm.debug.predictive_back_anim",
+                    SETTING_VALUE_OFF) == SETTING_VALUE_ON;
+    /** Predictive back animation developer option */
+    private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
     // TODO (b/241808055) Find a appropriate time to remove during refactor
     private static final boolean ENABLE_SHELL_TRANSITIONS = Transitions.ENABLE_SHELL_TRANSITIONS;
     /**
@@ -88,8 +93,6 @@
      */
     private static final long MAX_TRANSITION_DURATION = 2000;
 
-    private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
-
     /** True when a back gesture is ongoing */
     private boolean mBackGestureStarted = false;
 
@@ -143,53 +146,6 @@
         }
     };
 
-    /**
-     * Helper class to record the touch location for gesture start and latest.
-     */
-    private static class TouchTracker {
-        /**
-         * Location of the latest touch event
-         */
-        private float mLatestTouchX;
-        private float mLatestTouchY;
-        private int mSwipeEdge;
-        private float mProgressThreshold;
-
-        /**
-         * Location of the initial touch event of the back gesture.
-         */
-        private float mInitTouchX;
-        private float mInitTouchY;
-
-        void update(float touchX, float touchY, int swipeEdge) {
-            mLatestTouchX = touchX;
-            mLatestTouchY = touchY;
-            mSwipeEdge = swipeEdge;
-        }
-
-        void setGestureStartLocation(float touchX, float touchY) {
-            mInitTouchX = touchX;
-            mInitTouchY = touchY;
-        }
-
-        void setProgressThreshold(float progressThreshold) {
-            mProgressThreshold = progressThreshold;
-        }
-
-        float getProgress(float touchX) {
-            int deltaX = Math.round(touchX - mInitTouchX);
-            float progressThreshold = PROGRESS_THRESHOLD >= 0
-                    ? PROGRESS_THRESHOLD : mProgressThreshold;
-            return Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1);
-        }
-
-        void reset() {
-            mInitTouchX = 0;
-            mInitTouchY = 0;
-            mSwipeEdge = -1;
-        }
-    }
-
     public BackAnimationController(
             @NonNull ShellInit shellInit,
             @NonNull ShellController shellController,
@@ -221,6 +177,11 @@
         mTransitions = transitions;
     }
 
+    @VisibleForTesting
+    void setEnableUAnimation(boolean enable) {
+        IS_U_ANIMATION_ENABLED = enable;
+    }
+
     private void onInit() {
         setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
         createAdapter();
@@ -374,7 +335,8 @@
         if (mTransitionInProgress) {
             return;
         }
-        mTouchTracker.update(touchX, touchY, swipeEdge);
+
+        mTouchTracker.update(touchX, touchY);
         if (keyAction == MotionEvent.ACTION_DOWN) {
             if (!mBackGestureStarted) {
                 mShouldStartOnNextMoveEvent = true;
@@ -384,7 +346,7 @@
                 // Let the animation initialized here to make sure the onPointerDownOutsideFocus
                 // could be happened when ACTION_DOWN, it may change the current focus that we
                 // would access it when startBackNavigation.
-                onGestureStarted(touchX, touchY);
+                onGestureStarted(touchX, touchY, swipeEdge);
                 mShouldStartOnNextMoveEvent = false;
             }
             onMove(touchX, touchY, swipeEdge);
@@ -398,14 +360,14 @@
         }
     }
 
-    private void onGestureStarted(float touchX, float touchY) {
+    private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted);
         if (mBackGestureStarted || mBackNavigationInfo != null) {
             Log.e(TAG, "Animation is being initialized but is already started.");
             finishBackNavigation();
         }
 
-        mTouchTracker.setGestureStartLocation(touchX, touchY);
+        mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge);
         mBackGestureStarted = true;
 
         try {
@@ -428,12 +390,10 @@
         final IOnBackInvokedCallback targetCallback;
         final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
         if (shouldDispatchToAnimator) {
-            targetCallback = mAnimationDefinition.get(backType).getGestureStartedCallback();
+            mAnimationDefinition.get(backType).startGesture();
         } else {
             targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
-        }
-        if (shouldDispatchToAnimator) {
-            dispatchOnBackStarted(targetCallback);
+            dispatchOnBackStarted(targetCallback, mTouchTracker.createStartEvent(null));
         }
     }
 
@@ -441,12 +401,10 @@
         if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()) {
             return;
         }
-        mTouchTracker.update(touchX, touchY, swipeEdge);
-        float progress = mTouchTracker.getProgress(touchX);
-        int backType = mBackNavigationInfo.getType();
+        final BackEvent backEvent = mTouchTracker.createProgressEvent();
 
-        BackEvent backEvent = new BackEvent(touchX, touchY, progress, swipeEdge);
-        IOnBackInvokedCallback targetCallback = null;
+        int backType = mBackNavigationInfo.getType();
+        IOnBackInvokedCallback targetCallback;
         if (shouldDispatchToAnimator(backType)) {
             targetCallback = mAnimationDefinition.get(backType).getCallback();
         } else {
@@ -532,18 +490,21 @@
                 && mAnimationDefinition.contains(backType);
     }
 
-    private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) {
+    private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
+            BackEvent backEvent) {
         if (callback == null) {
             return;
         }
         try {
-            callback.onBackStarted();
+            if (shouldDispatchAnimation(callback)) {
+                callback.onBackStarted(backEvent);
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackStarted error: ", e);
         }
     }
 
-    private static void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
+    private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
         if (callback == null) {
             return;
         }
@@ -554,29 +515,39 @@
         }
     }
 
-    private static void dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
+    private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
         if (callback == null) {
             return;
         }
         try {
-            callback.onBackCancelled();
+            if (shouldDispatchAnimation(callback)) {
+                callback.onBackCancelled();
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackCancelled error: ", e);
         }
     }
 
-    private static void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
+    private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
             BackEvent backEvent) {
         if (callback == null) {
             return;
         }
         try {
-            callback.onBackProgressed(backEvent);
+            if (shouldDispatchAnimation(callback)) {
+                callback.onBackProgressed(backEvent);
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackProgressed error: ", e);
         }
     }
 
+    private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) {
+        return (IS_U_ANIMATION_ENABLED || callback == mAnimationDefinition.get(
+                BackNavigationInfo.TYPE_RETURN_TO_HOME).getCallback())
+                && mEnableAnimations.get();
+    }
+
     /**
      * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
      */
@@ -585,6 +556,7 @@
             return;
         }
         mTriggerBack = triggerBack;
+        mTouchTracker.setTriggerBack(triggerBack);
     }
 
     private void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
@@ -670,13 +642,18 @@
                     ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
                     runner.startAnimation(apps, wallpapers, nonApps,
                             BackAnimationController.this::onBackAnimationFinished);
+                    if (apps.length >= 1) {
+                        final int backType = mBackNavigationInfo.getType();
+                        IOnBackInvokedCallback targetCallback = mAnimationDefinition.get(backType)
+                                .getCallback();
+                        dispatchOnBackStarted(
+                                targetCallback, mTouchTracker.createStartEvent(apps[0]));
+                    }
 
                     if (!mBackGestureStarted) {
                         // if the down -> up gesture happened before animation start, we have to
                         // trigger the uninterruptible transition to finish the back animation.
-                        final BackEvent backFinish = new BackEvent(
-                                mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1,
-                                mTouchTracker.mSwipeEdge);
+                        final BackEvent backFinish = mTouchTracker.createProgressEvent(1);
                         startTransition();
                         runner.consumeIfGestureFinished(backFinish);
                     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index 12bbf73..c53fcfc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -79,9 +79,8 @@
         }
     }
 
-    IOnBackInvokedCallback getGestureStartedCallback() {
+    void startGesture() {
         mWaitingAnimation = true;
-        return mCallback;
     }
 
     boolean onGestureFinished(boolean triggerBack) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
new file mode 100644
index 0000000..ccfac65
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -0,0 +1,119 @@
+/*
+ * 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.wm.shell.back;
+
+import android.os.SystemProperties;
+import android.view.RemoteAnimationTarget;
+import android.window.BackEvent;
+
+/**
+ * Helper class to record the touch location for gesture and generate back events.
+ */
+class TouchTracker {
+    private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
+            "persist.wm.debug.predictive_back_progress_threshold";
+    private static final int PROGRESS_THRESHOLD = SystemProperties
+            .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
+    private float mProgressThreshold;
+    /**
+     * Location of the latest touch event
+     */
+    private float mLatestTouchX;
+    private float mLatestTouchY;
+    private boolean mTriggerBack;
+
+    /**
+     * Location of the initial touch event of the back gesture.
+     */
+    private float mInitTouchX;
+    private float mInitTouchY;
+    private float mStartThresholdX;
+    private int mSwipeEdge;
+    private boolean mCancelled;
+
+    void update(float touchX, float touchY) {
+        /**
+         * If back was previously cancelled but the user has started swiping in the forward
+         * direction again, restart back.
+         */
+        if (mCancelled && ((touchX > mLatestTouchX && mSwipeEdge == BackEvent.EDGE_LEFT)
+                || touchX < mLatestTouchX && mSwipeEdge == BackEvent.EDGE_RIGHT)) {
+            mCancelled = false;
+            mStartThresholdX = touchX;
+        }
+        mLatestTouchX = touchX;
+        mLatestTouchY = touchY;
+    }
+
+    void setTriggerBack(boolean triggerBack) {
+        if (mTriggerBack != triggerBack && !triggerBack) {
+            mCancelled = true;
+        }
+        mTriggerBack = triggerBack;
+    }
+
+    void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
+        mInitTouchX = touchX;
+        mInitTouchY = touchY;
+        mSwipeEdge = swipeEdge;
+        mStartThresholdX = mInitTouchX;
+    }
+
+    void reset() {
+        mInitTouchX = 0;
+        mInitTouchY = 0;
+        mStartThresholdX = 0;
+        mCancelled = false;
+        mTriggerBack = false;
+        mSwipeEdge = BackEvent.EDGE_LEFT;
+    }
+
+    BackEvent createStartEvent(RemoteAnimationTarget target) {
+        return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
+    }
+
+    BackEvent createProgressEvent() {
+        float progressThreshold = PROGRESS_THRESHOLD >= 0
+                ? PROGRESS_THRESHOLD : mProgressThreshold;
+        progressThreshold = progressThreshold == 0 ? 1 : progressThreshold;
+        float progress = 0;
+        // Progress is always 0 when back is cancelled and not restarted.
+        if (!mCancelled) {
+            // If back is committed, progress is the distance between the last and first touch
+            // point, divided by the max drag distance. Otherwise, it's the distance between
+            // the last touch point and the starting threshold, divided by max drag distance.
+            // The starting threshold is initially the first touch location, and updated to
+            // the location everytime back is restarted after being cancelled.
+            float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
+            float deltaX = Math.max(
+                    mSwipeEdge == BackEvent.EDGE_LEFT
+                            ? mLatestTouchX - startX
+                            : startX - mLatestTouchX,
+                    0);
+            progress = Math.min(Math.max(deltaX / progressThreshold, 0), 1);
+        }
+        return createProgressEvent(progress);
+    }
+
+    BackEvent createProgressEvent(float progress) {
+        return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
+    }
+
+    public void setProgressThreshold(float progressThreshold) {
+        mProgressThreshold = progressThreshold;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
index d6803e8..d3a9a67 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
@@ -52,7 +52,7 @@
             userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
         }
         Bitmap userBadgedBitmap = createIconBitmap(
-                userBadgedAppIcon, 1, BITMAP_GENERATION_MODE_WITH_SHADOW);
+                userBadgedAppIcon, 1, MODE_WITH_SHADOW);
         return createIconBitmap(userBadgedBitmap);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
index 5dab8a0..4ded3ea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
@@ -79,6 +79,6 @@
                 true /* shrinkNonAdaptiveIcons */,
                 null /* outscale */,
                 outScale);
-        return createIconBitmap(icon, outScale[0], BITMAP_GENERATION_MODE_WITH_SHADOW);
+        return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index afb64c9..43d3f36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -60,7 +60,7 @@
         FloatingContentCoordinator.FloatingContent {
 
     public static final boolean ENABLE_FLING_TO_DISMISS_PIP =
-            SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true);
+            SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", false);
     private static final String TAG = "PipMotionHelper";
     private static final boolean DEBUG = false;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index 8cee4f1..6ce981e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -432,7 +432,8 @@
                     final ShapeIconFactory factory = new ShapeIconFactory(
                             SplashscreenContentDrawer.this.mContext,
                             scaledIconDpi, mFinalIconSize);
-                    final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable);
+                    final Bitmap bitmap = factory.createScaledBitmap(iconDrawable,
+                            BaseIconFactory.MODE_DEFAULT);
                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                     createIconDrawable(new BitmapDrawable(bitmap), true,
                             mHighResIconProvider.mLoadInDetail);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 2b27bae..66d0a2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.transition;
 
-import static android.hardware.HardwareBuffer.RGBA_8888;
-import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
 import static android.util.RotationUtils.deltaRotation;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
@@ -37,8 +35,6 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.media.Image;
-import android.media.ImageReader;
 import android.util.Slog;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -50,12 +46,11 @@
 import android.window.TransitionInfo;
 
 import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
-import java.util.Arrays;
 
 /**
  * This class handles the rotation animation when the device is rotated.
@@ -173,7 +168,7 @@
                 t.setBuffer(mScreenshotLayer, hardwareBuffer);
                 t.show(mScreenshotLayer);
                 if (!isCustomRotate()) {
-                    mStartLuma = getMedianBorderLuma(hardwareBuffer, colorSpace);
+                    mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, colorSpace);
                 }
             }
 
@@ -404,93 +399,6 @@
         mTransactionPool.release(t);
     }
 
-    /**
-     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
-     * luminance at the borders of the bitmap
-     * @return the average luminance of all the pixels at the borders of the bitmap
-     */
-    private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
-        // Cannot read content from buffer with protected usage.
-        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
-                || hasProtectedContent(hardwareBuffer)) {
-            return 0;
-        }
-
-        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
-                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
-        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
-        Image image = ir.acquireLatestImage();
-        if (image == null || image.getPlanes().length == 0) {
-            return 0;
-        }
-
-        Image.Plane plane = image.getPlanes()[0];
-        ByteBuffer buffer = plane.getBuffer();
-        int width = image.getWidth();
-        int height = image.getHeight();
-        int pixelStride = plane.getPixelStride();
-        int rowStride = plane.getRowStride();
-        float[] borderLumas = new float[2 * width + 2 * height];
-
-        // Grab the top and bottom borders
-        int l = 0;
-        for (int x = 0; x < width; x++) {
-            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
-        }
-
-        // Grab the left and right borders
-        for (int y = 0; y < height; y++) {
-            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
-        }
-
-        // Cleanup
-        ir.close();
-
-        // Oh, is this too simple and inefficient for you?
-        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
-        Arrays.sort(borderLumas);
-        return borderLumas[borderLumas.length / 2];
-    }
-
-    /**
-     * @return whether the hardwareBuffer passed in is marked as protected.
-     */
-    private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
-        return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
-    }
-
-    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
-            int pixelStride, int rowStride) {
-        int offset = y * rowStride + x * pixelStride;
-        int pixel = 0;
-        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
-        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
-        pixel |= (buffer.get(offset + 2) & 0xff);       // B
-        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
-        return Color.valueOf(pixel).luminance();
-    }
-
-    /**
-     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
-     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
-     */
-    private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) {
-        if (surfaceControl ==  null) {
-            return 0;
-        }
-
-        Rect crop = new Rect(0, 0, bounds.width(), bounds.height());
-        ScreenCapture.ScreenshotHardwareBuffer buffer =
-                ScreenCapture.captureLayers(surfaceControl, crop, 1);
-        if (buffer == null) {
-            return 0;
-        }
-
-        return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
-    }
-
     private static void applyColor(int startColor, int endColor, float[] rgbFloat,
             float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
         final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
index e903897..f209521 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
@@ -33,6 +33,8 @@
     // This class is orientation-agnostic, so we compute both for later use
     public final float topTaskPercent;
     public final float leftTaskPercent;
+    public final float dividerWidthPercent;
+    public final float dividerHeightPercent;
     /**
      * If {@code true}, that means at the time of creation of this object, the
      * split-screened apps were vertically stacked. This is useful in scenarios like
@@ -62,8 +64,12 @@
             appsStackedVertically = false;
         }
 
-        leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right;
-        topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom;
+        float totalWidth = rightBottomBounds.right - leftTopBounds.left;
+        float totalHeight = rightBottomBounds.bottom - leftTopBounds.top;
+        leftTaskPercent = leftTopBounds.width() / totalWidth;
+        topTaskPercent = leftTopBounds.height() / totalHeight;
+        dividerWidthPercent = visualDividerBounds.width() / totalWidth;
+        dividerHeightPercent = visualDividerBounds.height() / totalHeight;
     }
 
     public SplitBounds(Parcel parcel) {
@@ -75,6 +81,8 @@
         appsStackedVertically = parcel.readBoolean();
         leftTopTaskId = parcel.readInt();
         rightBottomTaskId = parcel.readInt();
+        dividerWidthPercent = parcel.readInt();
+        dividerHeightPercent = parcel.readInt();
     }
 
     @Override
@@ -87,6 +95,8 @@
         parcel.writeBoolean(appsStackedVertically);
         parcel.writeInt(leftTopTaskId);
         parcel.writeInt(rightBottomTaskId);
+        parcel.writeFloat(dividerWidthPercent);
+        parcel.writeFloat(dividerHeightPercent);
     }
 
     @Override
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 87700ee..9d61c14 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
@@ -21,7 +21,6 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
-import android.graphics.Rect;
 import android.graphics.drawable.VectorDrawable;
 import android.os.Handler;
 import android.view.Choreographer;
@@ -43,22 +42,6 @@
  * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
  */
 public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
-    // The thickness of shadows of a window that has focus in DIP.
-    private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20;
-    // The thickness of shadows of a window that doesn't have focus in DIP.
-    private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5;
-
-    // Height of button (32dp)  + 2 * margin (5dp each)
-    private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42;
-    // Width of buttons (64dp) + handle (128dp) + padding (24dp total)
-    private static final int DECOR_CAPTION_WIDTH_IN_DIP = 216;
-    private static final int RESIZE_HANDLE_IN_DIP = 30;
-    private static final int RESIZE_CORNER_IN_DIP = 44;
-
-    private static final Rect EMPTY_OUTSET = new Rect();
-    private static final Rect RESIZE_HANDLE_OUTSET = new Rect(
-            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;
@@ -69,6 +52,7 @@
 
     private DragResizeInputListener mDragResizeListener;
 
+    private RelayoutParams mRelayoutParams = new RelayoutParams();
     private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
             new WindowDecoration.RelayoutResult<>();
 
@@ -114,19 +98,32 @@
 
     void relayout(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
-        final int shadowRadiusDp = taskInfo.isFocused
-                ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP;
-        final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode()
-                == WindowConfiguration.WINDOWING_MODE_FREEFORM;
-        final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable;
-        final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET;
+        final int shadowRadiusID = taskInfo.isFocused
+                ? R.dimen.freeform_decor_shadow_focused_thickness
+                : R.dimen.freeform_decor_shadow_unfocused_thickness;
+        final boolean isFreeform =
+                taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+        final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
 
         WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        relayout(taskInfo, R.layout.caption_window_decoration, oldRootView,
-                DECOR_CAPTION_HEIGHT_IN_DIP, DECOR_CAPTION_WIDTH_IN_DIP, outset, shadowRadiusDp,
-                startT, finishT, wct, mResult);
+
+        int outsetLeftId = R.dimen.freeform_resize_handle;
+        int outsetTopId = R.dimen.freeform_resize_handle;
+        int outsetRightId = R.dimen.freeform_resize_handle;
+        int outsetBottomId = R.dimen.freeform_resize_handle;
+
+        mRelayoutParams.reset();
+        mRelayoutParams.mRunningTaskInfo = taskInfo;
+        mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration;
+        mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
+        mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
+        mRelayoutParams.mShadowRadiusId = shadowRadiusID;
+        if (isDragResizeable) {
+            mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
+        }
+        relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
 
         mTaskOrganizer.applyTransaction(wct);
 
@@ -167,10 +164,12 @@
         }
 
         int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop();
-
+        int resize_handle = mResult.mRootView.getResources()
+                .getDimensionPixelSize(R.dimen.freeform_resize_handle);
+        int resize_corner = mResult.mRootView.getResources()
+                .getDimensionPixelSize(R.dimen.freeform_resize_corner);
         mDragResizeListener.setGeometry(
-                mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP),
-                (int) (mResult.mDensity * RESIZE_CORNER_IN_DIP), touchSlop);
+                mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
     }
 
     /**
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 bf863ea..b314163 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
@@ -19,11 +19,11 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.util.DisplayMetrics;
 import android.view.Display;
 import android.view.InsetsState;
 import android.view.LayoutInflater;
@@ -91,7 +91,7 @@
     SurfaceControl mTaskBackgroundSurface;
 
     SurfaceControl mCaptionContainerSurface;
-    private CaptionWindowManager mCaptionWindowManager;
+    private WindowlessWindowManager mCaptionWindowManager;
     private SurfaceControlViewHost mViewHost;
 
     private final Rect mCaptionInsetsRect = new Rect();
@@ -142,15 +142,14 @@
      */
     abstract void relayout(RunningTaskInfo taskInfo);
 
-    void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp,
-            float captionWidthDp, Rect outsetsDp, float shadowRadiusDp,
-            SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
-            WindowContainerTransaction wct, RelayoutResult<T> outResult) {
+    void relayout(RelayoutParams params, SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView,
+            RelayoutResult<T> outResult) {
         outResult.reset();
 
         final Configuration oldTaskConfig = mTaskInfo.getConfiguration();
-        if (taskInfo != null) {
-            mTaskInfo = taskInfo;
+        if (params.mRunningTaskInfo != null) {
+            mTaskInfo = params.mRunningTaskInfo;
         }
 
         if (!mTaskInfo.isVisible) {
@@ -159,7 +158,7 @@
             return;
         }
 
-        if (rootView == null && layoutResId == 0) {
+        if (rootView == null && params.mLayoutResId == 0) {
             throw new IllegalArgumentException("layoutResId and rootView can't both be invalid.");
         }
 
@@ -176,15 +175,15 @@
                 return;
             }
             mDecorWindowContext = mContext.createConfigurationContext(taskConfig);
-            if (layoutResId != 0) {
-                outResult.mRootView =
-                        (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+            if (params.mLayoutResId != 0) {
+                outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
+                                .inflate(params.mLayoutResId, null);
             }
         }
 
         if (outResult.mRootView == null) {
-            outResult.mRootView =
-                    (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+            outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
+                            .inflate(params.mLayoutResId , null);
         }
 
         // DecorationContainerSurface
@@ -200,18 +199,19 @@
         }
 
         final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
-        outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
-        final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity);
-        final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity);
+        final Resources resources = mDecorWindowContext.getResources();
+        final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId);
+        final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId);
         outResult.mWidth = taskBounds.width()
-                + (int) (outsetsDp.right * outResult.mDensity)
+                + loadDimensionPixelSize(resources, params.mOutsetRightId)
                 - decorContainerOffsetX;
         outResult.mHeight = taskBounds.height()
-                + (int) (outsetsDp.bottom * outResult.mDensity)
+                + loadDimensionPixelSize(resources, params.mOutsetBottomId)
                 - decorContainerOffsetY;
         startT.setPosition(
                         mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
-                .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
+                .setWindowCrop(mDecorationContainerSurface,
+                        outResult.mWidth, outResult.mHeight)
                 // TODO(b/244455401): Change the z-order when it's better organized
                 .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1)
                 .show(mDecorationContainerSurface);
@@ -226,12 +226,13 @@
                     .build();
         }
 
-        float shadowRadius = outResult.mDensity * shadowRadiusDp;
+        float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
         int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
         mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
         mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
         mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
-        startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height())
+        startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(),
+                        taskBounds.height())
                 .setShadowRadius(mTaskBackgroundSurface, shadowRadius)
                 .setColor(mTaskBackgroundSurface, mTmpColor)
                 // TODO(b/244455401): Change the z-order when it's better organized
@@ -248,8 +249,8 @@
                     .build();
         }
 
-        final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity);
-        final int captionWidth = (int) Math.ceil(captionWidthDp * outResult.mDensity);
+        final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
+        final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId);
 
         //Prevent caption from going offscreen if task is too high up
         final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2;
@@ -264,8 +265,9 @@
         if (mCaptionWindowManager == null) {
             // Put caption under a container surface because ViewRootImpl sets the destination frame
             // of windowless window layers and BLASTBufferQueue#update() doesn't support offset.
-            mCaptionWindowManager = new CaptionWindowManager(
-                    mTaskInfo.getConfiguration(), mCaptionContainerSurface);
+            mCaptionWindowManager = new WindowlessWindowManager(
+                    mTaskInfo.getConfiguration(), mCaptionContainerSurface,
+                    null /* hostInputToken */);
         }
 
         // Caption view
@@ -289,8 +291,10 @@
 
             // Caption insets
             mCaptionInsetsRect.set(taskBounds);
-            mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight - captionYPos;
-            wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES);
+            mCaptionInsetsRect.bottom =
+                    mCaptionInsetsRect.top + captionHeight - captionYPos;
+            wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect,
+                    CAPTION_INSETS_TYPES);
         } else {
             startT.hide(mCaptionContainerSurface);
         }
@@ -365,34 +369,67 @@
         releaseViews();
     }
 
+    private static int loadDimensionPixelSize(Resources resources, int resourceId) {
+        if (resourceId == Resources.ID_NULL) {
+            return 0;
+        }
+        return resources.getDimensionPixelSize(resourceId);
+    }
+
+    private static float loadDimension(Resources resources, int resourceId) {
+        if (resourceId == Resources.ID_NULL) {
+            return 0;
+        }
+        return resources.getDimension(resourceId);
+    }
+
+    static class RelayoutParams{
+        RunningTaskInfo mRunningTaskInfo;
+        int mLayoutResId;
+        int mCaptionHeightId;
+        int mCaptionWidthId;
+        int mShadowRadiusId;
+
+        int mOutsetTopId;
+        int mOutsetBottomId;
+        int mOutsetLeftId;
+        int mOutsetRightId;
+
+        void setOutsets(int leftId, int topId, int rightId, int bottomId) {
+            mOutsetLeftId = leftId;
+            mOutsetTopId = topId;
+            mOutsetRightId = rightId;
+            mOutsetBottomId = bottomId;
+        }
+
+        void reset() {
+            mLayoutResId = Resources.ID_NULL;
+            mCaptionHeightId = Resources.ID_NULL;
+            mCaptionWidthId = Resources.ID_NULL;
+            mShadowRadiusId = Resources.ID_NULL;
+
+            mOutsetTopId = Resources.ID_NULL;
+            mOutsetBottomId = Resources.ID_NULL;
+            mOutsetLeftId = Resources.ID_NULL;
+            mOutsetRightId = Resources.ID_NULL;
+        }
+    }
+
     static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
         int mWidth;
         int mHeight;
-        float mDensity;
         T mRootView;
 
         void reset() {
             mWidth = 0;
             mHeight = 0;
-            mDensity = 0;
             mRootView = null;
         }
     }
 
-    private static class CaptionWindowManager extends WindowlessWindowManager {
-        CaptionWindowManager(Configuration config, SurfaceControl rootSurface) {
-            super(config, rootSurface, null /* hostInputToken */);
-        }
-
-        @Override
-        public void setConfiguration(Configuration configuration) {
-            super.setConfiguration(configuration);
-        }
-    }
-
     interface SurfaceControlViewHostFactory {
         default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
             return new SurfaceControlViewHost(c, d, wmm);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
index fa783f2..45eae2e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
@@ -26,7 +26,6 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowBecomesInvisible
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -35,7 +34,6 @@
 import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
 import com.android.wm.shell.flicker.splitScreenDismissed
 import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
-import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -95,7 +93,9 @@
     fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
         primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false)
 
-    private fun secondaryAppBoundsIsFullscreenAtEnd_internal() {
+    @Presubmit
+    @Test
+    fun secondaryAppBoundsIsFullscreenAtEnd() {
         testSpec.assertLayers {
             this.isVisible(secondaryApp)
                 .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
@@ -117,20 +117,6 @@
 
     @Presubmit
     @Test
-    fun secondaryAppBoundsIsFullscreenAtEnd() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-        secondaryAppBoundsIsFullscreenAtEnd_internal()
-    }
-
-    @FlakyTest(bugId = 250528485)
-    @Test
-    fun secondaryAppBoundsIsFullscreenAtEnd_shellTransit() {
-        Assume.assumeTrue(isShellTransitionsEnabled)
-        secondaryAppBoundsIsFullscreenAtEnd_internal()
-    }
-
-    @Presubmit
-    @Test
     fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp)
 
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index 84a8c0a..73159c9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.splitscreen
 
-import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
@@ -146,19 +145,15 @@
         // robust enough to get the correct end state.
     }
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp)
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
         primaryApp,
@@ -166,9 +161,6 @@
         portraitPosTop = true
     )
 
-    // TODO(b/246490534): Move back to presubmit after withAppTransitionIdle is robust enough to
-    // get the correct end state.
-    @FlakyTest(bugId = 246490534)
     @Test
     fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
         secondaryApp,
@@ -176,11 +168,9 @@
         portraitPosTop = false
     )
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
new file mode 100644
index 0000000..8949a75
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.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>
+    <!-- Resources used in WindowDecorationTests -->
+    <dimen name="test_freeform_decor_caption_height">32dp</dimen>
+    <dimen name="test_freeform_decor_caption_width">216dp</dimen>
+    <dimen name="test_window_decor_left_outset">10dp</dimen>
+    <dimen name="test_window_decor_top_outset">20dp</dimen>
+    <dimen name="test_window_decor_right_outset">30dp</dimen>
+    <dimen name="test_window_decor_bottom_outset">40dp</dimen>
+    <dimen name="test_window_decor_shadow_radius">5dp</dimen>
+    <dimen name="test_window_decor_resize_handle">10dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 6484b07..7896247 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -128,6 +128,7 @@
                 mShellExecutor, new Handler(mTestableLooper.getLooper()),
                 mActivityTaskManager, mContext,
                 mContentResolver, mTransitions);
+        mController.setEnableUAnimation(true);
         mShellInit.init();
         mEventTime = 0;
         mShellExecutor.flushAll();
@@ -206,10 +207,9 @@
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
-        ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
-        verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
+        verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class));
 
         // Check that back invocation is dispatched.
         mController.setTriggerBack(true);   // Fake trigger back
@@ -236,11 +236,11 @@
 
         triggerBackGesture();
 
-        verify(appCallback, never()).onBackStarted();
+        verify(appCallback, never()).onBackStarted(any(BackEvent.class));
         verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
         verify(appCallback, times(1)).onBackInvoked();
 
-        verify(mIOnBackInvokedCallback, never()).onBackStarted();
+        verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class));
         verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
         verify(mIOnBackInvokedCallback, never()).onBackInvoked();
         verify(mBackAnimationRunner, never()).onAnimationStart(
@@ -279,7 +279,7 @@
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
     }
 
@@ -301,9 +301,8 @@
 
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
-
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
     }
 
 
@@ -318,7 +317,7 @@
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
 
         // Check that back invocation is dispatched.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
new file mode 100644
index 0000000..3aefc3f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.wm.shell.back;
+
+import static org.junit.Assert.assertEquals;
+
+import android.window.BackEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class TouchTrackerTest {
+    private static final float FAKE_THRESHOLD = 400;
+    private static final float INITIAL_X_LEFT_EDGE = 5;
+    private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE;
+    private TouchTracker mTouchTracker;
+
+    @Before
+    public void setUp() throws Exception {
+        mTouchTracker = new TouchTracker();
+        mTouchTracker.setProgressThreshold(FAKE_THRESHOLD);
+    }
+
+    @Test
+    public void generatesProgress_onStart() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
+        BackEvent event = mTouchTracker.createStartEvent(null);
+        assertEquals(event.getProgress(), 0f, 0f);
+    }
+
+    @Test
+    public void generatesProgress_leftEdge() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
+        float touchX = 10;
+
+        // Pre-commit
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+
+        // Post-commit
+        touchX += 100;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+
+        // Cancel
+        touchX -= 10;
+        mTouchTracker.setTriggerBack(false);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Cancel more
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restart
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restarted, but pre-commit
+        float restartX = touchX;
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f);
+
+        // Restarted, post-commit
+        touchX += 10;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+    }
+
+    @Test
+    public void generatesProgress_rightEdge() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT);
+        float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge
+
+        // Pre-commit
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Post-commit
+        touchX -= 100;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Cancel
+        touchX += 10;
+        mTouchTracker.setTriggerBack(false);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Cancel more
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restart
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restarted, but pre-commit
+        float restartX = touchX;
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Restarted, post-commit
+        touchX -= 10;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+    }
+
+    private float getProgress() {
+        return mTouchTracker.createProgressEvent().getProgress();
+    }
+}
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 fa62b9c..4d37e5d 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
@@ -50,11 +50,13 @@
 import android.window.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.tests.R;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -76,13 +78,9 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class WindowDecorationTests extends ShellTestCase {
-    private static final int CAPTION_HEIGHT_DP = 32;
-    private static final int CAPTION_WIDTH_DP = 216;
-    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 =
             new WindowDecoration.RelayoutResult<>();
 
@@ -104,6 +102,7 @@
     private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>();
     private SurfaceControl.Transaction mMockSurfaceControlStartT;
     private SurfaceControl.Transaction mMockSurfaceControlFinishT;
+    private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams();
 
     @Before
     public void setUp() {
@@ -147,7 +146,11 @@
         // 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);
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
 
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -197,8 +200,11 @@
         // 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);
-
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
 
@@ -226,16 +232,17 @@
         verify(mMockSurfaceControlStartT).show(captionContainerSurface);
 
         verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
+
         verify(mMockSurfaceControlViewHost)
                 .setView(same(mMockView),
                         argThat(lp -> lp.height == 64
-                                && lp.width == 300
+                                && lp.width == 432
                                 && (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 Rect(100, 300, 400, 332),
                             new int[] { InsetsState.ITYPE_CAPTION_BAR });
         }
 
@@ -248,7 +255,6 @@
 
         assertEquals(380, mRelayoutResult.mWidth);
         assertEquals(220, mRelayoutResult.mHeight);
-        assertEquals(2, mRelayoutResult.mDensity, 0.f);
     }
 
     @Test
@@ -287,7 +293,11 @@
         // 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);
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
 
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -358,7 +368,8 @@
 
     private TestWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
-        return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
+        return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(),
+                mMockDisplayController, mMockShellTaskOrganizer,
                 taskInfo, testSurface,
                 new MockObjectSupplier<>(mMockSurfaceControlBuilders,
                         () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))),
@@ -410,9 +421,13 @@
 
         @Override
         void relayout(ActivityManager.RunningTaskInfo taskInfo) {
-            relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP,
-                    CAPTION_WIDTH_DP, mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT,
-                    mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult);
+            mRelayoutParams.mLayoutResId = 0;
+            mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height;
+            mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width;
+            mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius;
+
+            relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
+                    mMockWindowContainerTransaction, mMockView, mRelayoutResult);
         }
     }
 }
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 2547a963..d975e96 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -6632,8 +6632,8 @@
             }
         }
         if (k == ports.size()) {
-            // this hould never happen
-            Log.e(TAG, "updatePortConfig port not found for handle: "+port.handle().id());
+            // This can happen in case of stale audio patch referring to a removed device and is
+            // handled by the caller.
             return null;
         }
         AudioGainConfig gainCfg = portCfg.gain();
diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java
index 39b3d0b..0291f64 100644
--- a/media/java/android/media/ImageWriter.java
+++ b/media/java/android/media/ImageWriter.java
@@ -264,10 +264,9 @@
         if (useSurfaceImageFormatInfo) {
             // nativeInit internally overrides UNKNOWN format. So does surface format query after
             // nativeInit and before getEstimatedNativeAllocBytes().
-            imageFormat = SurfaceUtils.getSurfaceFormat(surface);
-            mDataSpace = dataSpace = PublicFormatUtils.getHalDataspace(imageFormat);
-            mHardwareBufferFormat =
-                hardwareBufferFormat = PublicFormatUtils.getHalFormat(imageFormat);
+            mHardwareBufferFormat = hardwareBufferFormat = SurfaceUtils.getSurfaceFormat(surface);
+            mDataSpace = dataSpace = SurfaceUtils.getSurfaceDataspace(surface);
+            imageFormat = PublicFormatUtils.getPublicFormat(hardwareBufferFormat, dataSpace);
         }
 
         // Estimate the native buffer allocation size and register it so it gets accounted for
diff --git a/media/java/android/media/MediaCrypto.java b/media/java/android/media/MediaCrypto.java
index 889a5f7..1930262 100644
--- a/media/java/android/media/MediaCrypto.java
+++ b/media/java/android/media/MediaCrypto.java
@@ -75,14 +75,17 @@
     public final native boolean requiresSecureDecoderComponent(@NonNull String mime);
 
     /**
-     * Associate a MediaDrm session with this MediaCrypto instance.  The
-     * MediaDrm session is used to securely load decryption keys for a
-     * crypto scheme.  The crypto keys loaded through the MediaDrm session
+     * Associate a new MediaDrm session with this MediaCrypto instance.
+     *
+     * <p>The MediaDrm session is used to securely load decryption keys for a
+     * crypto scheme. The crypto keys loaded through the MediaDrm session
      * may be selected for use during the decryption operation performed
      * by {@link android.media.MediaCodec#queueSecureInputBuffer} by specifying
-     * their key ids in the {@link android.media.MediaCodec.CryptoInfo#key} field.
-     * @param sessionId the MediaDrm sessionId to associate with this
-     * MediaCrypto instance
+     * their key IDs in the {@link android.media.MediaCodec.CryptoInfo#key} field.
+     *
+     * @param sessionId The MediaDrm sessionId to associate with this MediaCrypto
+     *         instance. The session's scheme must match the scheme UUID used when
+     *         constructing this MediaCrypto instance.
      * @throws MediaCryptoException on failure to set the sessionId
      */
     public final native void setMediaDrmSession(@NonNull byte[] sessionId)
diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java
index 74c5499..ee82588 100644
--- a/media/java/android/media/midi/MidiManager.java
+++ b/media/java/android/media/midi/MidiManager.java
@@ -240,8 +240,7 @@
      * @param handler The {@link android.os.Handler Handler} that will be used for delivering the
      *                device notifications. If handler is null, then the thread used for the
      *                callback is unspecified.
-     * @deprecated Use the {@link #registerDeviceCallback}
-     *             method with Executor and transport instead.
+     * @deprecated Use {@link #registerDeviceCallback(int, Executor, DeviceCallback)} instead.
      */
     @Deprecated
     public void registerDeviceCallback(DeviceCallback callback, Handler handler) {
diff --git a/packages/CarrierDefaultApp/Android.bp b/packages/CarrierDefaultApp/Android.bp
index fc753da..6990ad0 100644
--- a/packages/CarrierDefaultApp/Android.bp
+++ b/packages/CarrierDefaultApp/Android.bp
@@ -10,6 +10,7 @@
 android_app {
     name: "CarrierDefaultApp",
     srcs: ["src/**/*.java"],
+    libs: ["SliceStore"],
     platform_apis: true,
     certificate: "platform",
 }
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml
index 632dfb3..a5b104b 100644
--- a/packages/CarrierDefaultApp/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/AndroidManifest.xml
@@ -28,6 +28,7 @@
     <uses-permission android:name="android.permission.NETWORK_BYPASS_PRIVATE_DNS" />
     <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
 
     <application
         android:label="@string/app_name"
@@ -71,5 +72,22 @@
                 <data android:host="*" />
             </intent-filter>
         </activity-alias>
+
+        <receiver android:name="com.android.carrierdefaultapp.SliceStoreBroadcastReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.phone.slicestore.action.START_SLICE_STORE" />
+                <action android:name="com.android.phone.slicestore.action.SLICE_STORE_RESPONSE_TIMEOUT" />
+                <action android:name="com.android.phone.slicestore.action.NOTIFICATION_CANCELED" />
+            </intent-filter>
+        </receiver>
+        <activity android:name="com.android.carrierdefaultapp.SliceStoreActivity"
+                  android:label="@string/slice_store_label"
+                  android:exported="true"
+                  android:configChanges="keyboardHidden|orientation|screenSize">
+            <intent-filter>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
new file mode 100644
index 0000000..ad8a21c
--- /dev/null
+++ b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
@@ -0,0 +1,23 @@
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path android:fillColor="@android:color/white"
+          android:pathData="M3,17V15H8Q8,15 8,15Q8,15 8,15V13Q8,13 8,13Q8,13 8,13H3V7H10V9H5V11H8Q8.825,11 9.413,11.587Q10,12.175 10,13V15Q10,15.825 9.413,16.413Q8.825,17 8,17ZM21,11V15Q21,15.825 20.413,16.413Q19.825,17 19,17H14Q13.175,17 12.588,16.413Q12,15.825 12,15V9Q12,8.175 12.588,7.587Q13.175,7 14,7H19Q19.825,7 20.413,7.587Q21,8.175 21,9H14Q14,9 14,9Q14,9 14,9V15Q14,15 14,15Q14,15 14,15H19Q19,15 19,15Q19,15 19,15V13H16.5V11Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml
index 65a7cec..ce88a40 100644
--- a/packages/CarrierDefaultApp/res/values/strings.xml
+++ b/packages/CarrierDefaultApp/res/values/strings.xml
@@ -13,4 +13,18 @@
     <string name="ssl_error_warning">The network you&#8217;re trying to join has security issues.</string>
     <string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string>
     <string name="ssl_error_continue">Continue anyway via browser</string>
+
+    <!-- Telephony notification channel name for network boost notifications. -->
+    <string name="network_boost_notification_channel">Network Boost</string>
+    <!-- Notification title text for the network boost notification. -->
+    <string name="network_boost_notification_title">%s recommends a data boost</string>
+    <!-- Notification detail text for the network boost notification. -->
+    <string name="network_boost_notification_detail">Buy a network boost for better performance</string>
+    <!-- Notification button text to cancel the network boost notification. -->
+    <string name="network_boost_notification_button_not_now">Not now</string>
+    <!-- Notification button text to manage the network boost notification. -->
+    <string name="network_boost_notification_button_manage">Manage</string>
+
+    <!-- Label to display when the slice store opens. -->
+    <string name="slice_store_label">Purchase a network boost.</string>
 </resources>
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
new file mode 100644
index 0000000..24cb5f9
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.webkit.WebView;
+
+import com.android.phone.slicestore.SliceStore;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Activity that launches when the user clicks on the network boost notification.
+ */
+public class SliceStoreActivity extends Activity {
+    private static final String TAG = "SliceStoreActivity";
+
+    private URL mUrl;
+    private WebView mWebView;
+    private int mPhoneId;
+    private int mSubId;
+    private @TelephonyManager.PremiumCapability int mCapability;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+        mPhoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID,
+                SubscriptionManager.INVALID_PHONE_INDEX);
+        mSubId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        mCapability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        mUrl = getUrl();
+        logd("onCreate: mPhoneId=" + mPhoneId + ", mSubId=" + mSubId + ", mCapability="
+                + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                + ", mUrl=" + mUrl);
+        getApplicationContext().getSystemService(NotificationManager.class)
+                .cancel(SliceStoreBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability);
+        if (!SliceStoreBroadcastReceiver.isIntentValid(intent)) {
+            loge("Not starting SliceStoreActivity with an invalid Intent: " + intent);
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED);
+            finishAndRemoveTask();
+            return;
+        }
+        if (mUrl == null) {
+            loge("Unable to create a URL from carrier configs.");
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR);
+            finishAndRemoveTask();
+            return;
+        }
+        if (mSubId != SubscriptionManager.getDefaultSubscriptionId()) {
+            loge("Unable to start SliceStore on the non-default data subscription: " + mSubId);
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA);
+            finishAndRemoveTask();
+            return;
+        }
+
+        SliceStoreBroadcastReceiver.updateSliceStoreActivity(mCapability, this);
+
+        mWebView = new WebView(getApplicationContext());
+        setContentView(mWebView);
+        mWebView.loadUrl(mUrl.toString());
+        // TODO(b/245882601): Get back response from WebView
+    }
+
+    @Override
+    protected void onDestroy() {
+        logd("onDestroy: User canceled the purchase by closing the application.");
+        SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                getIntent(), SliceStore.EXTRA_INTENT_CANCELED);
+        SliceStoreBroadcastReceiver.removeSliceStoreActivity(mCapability);
+        super.onDestroy();
+    }
+
+    private @Nullable URL getUrl() {
+        String url = getApplicationContext().getSystemService(CarrierConfigManager.class)
+                .getConfigForSubId(mSubId).getString(
+                        CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
+        try {
+            return new URL(url);
+        } catch (MalformedURLException e) {
+            loge("Invalid URL: " + url);
+        }
+        return null;
+    }
+
+    private static void logd(@NonNull String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void loge(@NonNull String s) {
+        Log.e(TAG, s);
+    }
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
new file mode 100644
index 0000000..7eb851d
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
@@ -0,0 +1,315 @@
+/*
+ * 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.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.UserHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebView;
+
+import com.android.phone.slicestore.SliceStore;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The SliceStoreBroadcastReceiver listens for {@link SliceStore#ACTION_START_SLICE_STORE} from the
+ * SliceStore in the phone process to start the SliceStore application. It displays the network
+ * boost notification to the user and will start the {@link SliceStoreActivity} to display the
+ * {@link WebView} to purchase network boosts from the user's carrier.
+ */
+public class SliceStoreBroadcastReceiver extends BroadcastReceiver{
+    private static final String TAG = "SliceStoreBroadcastReceiver";
+
+    /** Weak references to {@link SliceStoreActivity} for each capability, if it exists. */
+    private static final Map<Integer, WeakReference<SliceStoreActivity>> sSliceStoreActivities =
+            new HashMap<>();
+
+    /** Channel ID for the network boost notification. */
+    private static final String NETWORK_BOOST_NOTIFICATION_CHANNEL_ID = "network_boost";
+    /** Tag for the network boost notification. */
+    public static final String NETWORK_BOOST_NOTIFICATION_TAG = "SliceStore.Notification";
+    /** Action for when the user clicks the "Not now" button on the network boost notification. */
+    private static final String ACTION_NOTIFICATION_CANCELED =
+            "com.android.phone.slicestore.action.NOTIFICATION_CANCELED";
+
+    /**
+     * Create a weak reference to {@link SliceStoreActivity}. The reference will be removed when
+     * {@link SliceStoreActivity#onDestroy()} is called.
+     *
+     * @param capability The premium capability requested.
+     * @param sliceStoreActivity The instance of SliceStoreActivity.
+     */
+    public static void updateSliceStoreActivity(@TelephonyManager.PremiumCapability int capability,
+            @NonNull SliceStoreActivity sliceStoreActivity) {
+        sSliceStoreActivities.put(capability, new WeakReference<>(sliceStoreActivity));
+    }
+
+    /**
+     * Remove the weak reference to {@link SliceStoreActivity} when
+     * {@link SliceStoreActivity#onDestroy()} is called.
+     *
+     * @param capability The premium capability requested.
+     */
+    public static void removeSliceStoreActivity(
+            @TelephonyManager.PremiumCapability int capability) {
+        sSliceStoreActivities.remove(capability);
+    }
+
+    /**
+     * Send the PendingIntent containing the corresponding SliceStore response.
+     *
+     * @param intent The Intent containing the PendingIntent extra.
+     * @param extra The extra to get the PendingIntent to send.
+     */
+    public static void sendSliceStoreResponse(@NonNull Intent intent, @NonNull String extra) {
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("PendingIntent does not exist for extra: " + extra);
+            return;
+        }
+        try {
+            pendingIntent.send();
+        } catch (PendingIntent.CanceledException e) {
+            loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e);
+        }
+    }
+
+    /**
+     * Check whether the Intent is valid and can be used to complete purchases in the SliceStore.
+     * This checks that all necessary extras exist and that the values are valid.
+     *
+     * @param intent The intent to check
+     * @return {@code true} if the intent is valid and {@code false} otherwise.
+     */
+    public static boolean isIntentValid(@NonNull Intent intent) {
+        int phoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID,
+                SubscriptionManager.INVALID_PHONE_INDEX);
+        if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
+            loge("isIntentValid: invalid phone index: " + phoneId);
+            return false;
+        }
+
+        int subId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            loge("isIntentValid: invalid subscription ID: " + subId);
+            return false;
+        }
+
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        if (capability == SliceStore.PREMIUM_CAPABILITY_INVALID) {
+            loge("isIntentValid: invalid premium capability: " + capability);
+            return false;
+        }
+
+        String appName = intent.getStringExtra(SliceStore.EXTRA_REQUESTING_APP_NAME);
+        if (TextUtils.isEmpty(appName)) {
+            loge("isIntentValid: empty requesting application name: " + appName);
+            return false;
+        }
+
+        return isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CANCELED)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA);
+    }
+
+    private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) {
+        String intentType = getPendingIntentType(extra);
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("isPendingIntentValid: " + intentType + " intent not found.");
+            return false;
+        } else if (pendingIntent.getCreatorPackage().equals(TelephonyManager.PHONE_PROCESS_NAME)) {
+            return true;
+        }
+        loge("isPendingIntentValid: " + intentType + " intent was created by "
+                + pendingIntent.getCreatorPackage() + " instead of the phone process.");
+        return false;
+    }
+
+    @NonNull private static String getPendingIntentType(@NonNull String extra) {
+        switch (extra) {
+            case SliceStore.EXTRA_INTENT_CANCELED: return "canceled";
+            case SliceStore.EXTRA_INTENT_CARRIER_ERROR: return "carrier error";
+            case SliceStore.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
+            case SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA: return "not default data";
+            default: {
+                loge("Unknown pending intent extra: " + extra);
+                return "unknown(" + extra + ")";
+            }
+        }
+    }
+
+    @Override
+    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+        logd("onReceive intent: " + intent.getAction());
+        switch (intent.getAction()) {
+            case SliceStore.ACTION_START_SLICE_STORE:
+                onDisplayBoosterNotification(context, intent);
+                break;
+            case SliceStore.ACTION_SLICE_STORE_RESPONSE_TIMEOUT:
+                onTimeout(context, intent);
+                break;
+            case ACTION_NOTIFICATION_CANCELED:
+                onUserCanceled(context, intent);
+                break;
+            default:
+                loge("Received unknown action: " + intent.getAction());
+        }
+    }
+
+    private void onDisplayBoosterNotification(@NonNull Context context, @NonNull Intent intent) {
+        if (!isIntentValid(intent)) {
+            sendSliceStoreResponse(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED);
+            return;
+        }
+
+        context.getSystemService(NotificationManager.class).createNotificationChannel(
+                new NotificationChannel(NETWORK_BOOST_NOTIFICATION_CHANNEL_ID,
+                        context.getResources().getString(
+                                R.string.network_boost_notification_channel),
+                        NotificationManager.IMPORTANCE_DEFAULT));
+
+        Notification notification =
+                new Notification.Builder(context, NETWORK_BOOST_NOTIFICATION_CHANNEL_ID)
+                        .setContentTitle(String.format(context.getResources().getString(
+                                R.string.network_boost_notification_title),
+                                intent.getStringExtra(SliceStore.EXTRA_REQUESTING_APP_NAME)))
+                        .setContentText(context.getResources().getString(
+                                R.string.network_boost_notification_detail))
+                        .setSmallIcon(R.drawable.ic_network_boost)
+                        .setContentIntent(createContentIntent(context, intent, 1))
+                        .setDeleteIntent(intent.getParcelableExtra(
+                                SliceStore.EXTRA_INTENT_CANCELED, PendingIntent.class))
+                        // Add an action for the "Not now" button, which has the same behavior as
+                        // the user canceling or closing the notification.
+                        .addAction(new Notification.Action.Builder(
+                                Icon.createWithResource(context, R.drawable.ic_network_boost),
+                                context.getResources().getString(
+                                        R.string.network_boost_notification_button_not_now),
+                                createCanceledIntent(context, intent)).build())
+                        // Add an action for the "Manage" button, which has the same behavior as
+                        // the user clicking on the notification.
+                        .addAction(new Notification.Action.Builder(
+                                Icon.createWithResource(context, R.drawable.ic_network_boost),
+                                context.getResources().getString(
+                                        R.string.network_boost_notification_button_manage),
+                                createContentIntent(context, intent, 2)).build())
+                        .build();
+
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("Display the booster notification for capability "
+                + TelephonyManager.convertPremiumCapabilityToString(capability));
+        context.getSystemService(NotificationManager.class).notifyAsUser(
+                NETWORK_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
+    }
+
+    /**
+     * Create the intent for when the user clicks on the "Manage" button on the network boost
+     * notification or the notification itself. This will open {@link SliceStoreActivity}.
+     *
+     * @param context The Context to create the intent for.
+     * @param intent The source Intent used to launch the SliceStore application.
+     * @param requestCode The request code for the PendingIntent.
+     *
+     * @return The intent to start {@link SliceStoreActivity}.
+     */
+    @NonNull private PendingIntent createContentIntent(@NonNull Context context,
+            @NonNull Intent intent, int requestCode) {
+        Intent i = new Intent(context, SliceStoreActivity.class);
+        i.setComponent(ComponentName.unflattenFromString(
+                "com.android.carrierdefaultapp/.SliceStoreActivity"));
+        i.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
+                | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        i.putExtras(intent);
+        return PendingIntent.getActivityAsUser(context, requestCode, i,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, null /* options */,
+                UserHandle.CURRENT);
+    }
+
+    /**
+     * Create the canceled intent for when the user clicks the "Not now" button on the network boost
+     * notification. This will send {@link #ACTION_NOTIFICATION_CANCELED} and has the same function
+     * as if the user had canceled or removed the notification.
+     *
+     * @param context The Context to create the intent for.
+     * @param intent The source Intent used to launch the SliceStore application.
+     *
+     * @return The canceled intent.
+     */
+    @NonNull private PendingIntent createCanceledIntent(@NonNull Context context,
+            @NonNull Intent intent) {
+        Intent i = new Intent(ACTION_NOTIFICATION_CANCELED);
+        i.setComponent(ComponentName.unflattenFromString(
+                "com.android.carrierdefaultapp/.SliceStoreBroadcastReceiver"));
+        i.putExtras(intent);
+        return PendingIntent.getBroadcast(context, 0, i,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+    }
+
+    private void onTimeout(@NonNull Context context, @NonNull Intent intent) {
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("Purchase capability " + TelephonyManager.convertPremiumCapabilityToString(capability)
+                + " timed out.");
+        if (sSliceStoreActivities.get(capability) == null) {
+            // Notification is still active
+            logd("Closing booster notification since the user did not respond in time.");
+            context.getSystemService(NotificationManager.class).cancelAsUser(
+                    NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        } else {
+            // Notification was dismissed but SliceStoreActivity is still active
+            logd("Closing SliceStore WebView since the user did not complete the purchase "
+                    + "in time.");
+            sSliceStoreActivities.get(capability).get().finishAndRemoveTask();
+            // TODO: Display a toast to indicate timeout for better UX?
+        }
+    }
+
+    private void onUserCanceled(@NonNull Context context, @NonNull Intent intent) {
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability));
+        context.getSystemService(NotificationManager.class)
+                .cancelAsUser(NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        sendSliceStoreResponse(intent, SliceStore.EXTRA_INTENT_CANCELED);
+    }
+
+    private static void logd(String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void loge(String s) {
+        Log.e(TAG, s);
+    }
+}
diff --git a/packages/CompanionDeviceManager/TEST_MAPPING b/packages/CompanionDeviceManager/TEST_MAPPING
deleted file mode 100644
index 63f54fa..0000000
--- a/packages/CompanionDeviceManager/TEST_MAPPING
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "CtsOsTestCases",
-      "options": [
-        {
-          "include-filter": "android.os.cts.CompanionDeviceManagerTest"
-        }
-      ]
-    }
-  ]
-}
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 2c24bf1..92ce772 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -1,4 +1,5 @@
-<resources>
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
   <string name="app_name">CredentialManager</string>
   <string name="string_cancel">Cancel</string>
   <string name="string_continue">Continue</string>
@@ -12,4 +13,7 @@
   <string name="choose_create_option_title">Create a passkey at</string>
   <string name="choose_sign_in_title">Use saved sign in</string>
   <string name="create_passkey_at">Create passkey at</string>
+  <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoName">%1$s</xliff:g> for all your sign-ins?</string>
+  <string name="set_as_default">Set as default</string>
+  <string name="use_once">Use once</string>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 489cc27..93eaeb3 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -21,6 +21,7 @@
 import android.app.slice.SliceSpec
 import android.content.Context
 import android.content.Intent
+import android.credentials.ui.Constants
 import android.credentials.ui.Entry
 import android.credentials.ui.ProviderData
 import android.credentials.ui.RequestInfo
@@ -31,6 +32,7 @@
 import android.os.ResultReceiver
 import com.android.credentialmanager.createflow.CreatePasskeyUiState
 import com.android.credentialmanager.createflow.CreateScreenState
+import com.android.credentialmanager.createflow.RequestDisplayInfo
 import com.android.credentialmanager.getflow.GetCredentialUiState
 import com.android.credentialmanager.getflow.GetScreenState
 
@@ -60,7 +62,7 @@
     ) ?: testProviderList()
 
     resultReceiver = intent.getParcelableExtra(
-      RequestInfo.EXTRA_RESULT_RECEIVER,
+      Constants.EXTRA_RESULT_RECEIVER,
       ResultReceiver::class.java
     )
   }
@@ -94,9 +96,12 @@
 
   fun createPasskeyInitialUiState(): CreatePasskeyUiState {
     val providerList = CreateFlowUtils.toProviderList(providerList, context)
+    val requestDisplayInfo = RequestDisplayInfo(
+      "Elisa Beckett", "beckett-bakert@gmail.com", "TYPE_CREATE")
     return CreatePasskeyUiState(
       providers = providerList,
       currentScreenState = CreateScreenState.PASSKEY_INTRO,
+      requestDisplayInfo,
     )
   }
 
@@ -118,46 +123,42 @@
   // TODO: below are prototype functionalities. To be removed for productionization.
   private fun testProviderList(): List<ProviderData> {
     return listOf(
-      ProviderData(
+      ProviderData.Builder(
         "com.google",
-        listOf<Entry>(
-          newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(2, "elisa.work@google.com", "Elisa Backett Work",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(
-          newEntry(3, "Go to Settings", "",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(4, "Switch Account", "",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        null
-      ),
-      ProviderData(
+        "Google Password Manager",
+        Icon.createWithResource(context, R.drawable.ic_launcher_foreground))
+        .setCredentialEntries(
+          listOf<Entry>(
+            newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett",
+                     "20 passwords and 7 passkeys saved"),
+            newEntry(2, "elisa.work@google.com", "Elisa Backett Work",
+                     "20 passwords and 7 passkeys saved"),
+          )
+        ).setActionChips(
+          listOf<Entry>(
+            newEntry(3, "Go to Settings", "",
+                     "20 passwords and 7 passkeys saved"),
+            newEntry(4, "Switch Account", "",
+                     "20 passwords and 7 passkeys saved"),
+          ),
+        ).build(),
+      ProviderData.Builder(
         "com.dashlane",
-        listOf<Entry>(
-          newEntry(5, "elisa.beckett@dashlane.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(6, "elisa.work@dashlane.com", "Elisa Backett Work",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(
-          newEntry(7, "Manage Accounts", "Manage your accounts in the dashlane app",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        null
-      ),
-      ProviderData(
-        "com.lastpass",
-        listOf<Entry>(
-          newEntry(8, "elisa.beckett@lastpass.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(),
-        null
-      )
-
+        "Dashlane",
+        Icon.createWithResource(context, R.drawable.ic_launcher_foreground))
+        .setCredentialEntries(
+          listOf<Entry>(
+            newEntry(1, "elisa.beckett@dashlane.com", "Elisa Backett",
+                     "20 passwords and 7 passkeys saved"),
+            newEntry(2, "elisa.work@dashlane.com", "Elisa Backett Work",
+                     "20 passwords and 7 passkeys saved"),
+          )
+        ).setActionChips(
+          listOf<Entry>(
+            newEntry(3, "Manage Accounts", "Manage your accounts in the dashlane app",
+                     "20 passwords and 7 passkeys saved"),
+          ),
+        ).build(),
     )
   }
 
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 19820d6..e291cc2 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -34,6 +34,21 @@
   val usageData: String
 )
 
+data class RequestDisplayInfo(
+  val userName: String,
+  val displayName: String,
+  val type: String,
+)
+
+/**
+ * This is initialized to be the most recent used. Can then be changed if
+ * user selects a different entry on the more option page.
+ */
+data class ActiveEntry (
+  val activeProvider: ProviderInfo,
+  val activeCreateOptionInfo: CreateOptionInfo,
+)
+
 /** The name of the current screen. */
 enum class CreateScreenState {
   PASSKEY_INTRO,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index f4d60b5..fbbc3ac 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -70,19 +70,21 @@
           onProviderSelected = {viewModel.onProviderSelected(it)}
         )
         CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
-          providerInfo = uiState.selectedProvider!!,
-          onOptionSelected = {viewModel.onCreateOptionSelected(it)},
+          requestDisplayInfo = uiState.requestDisplayInfo,
+          providerInfo = uiState.activeEntry?.activeProvider!!,
+          onOptionSelected = {viewModel.onPrimaryCreateOptionInfoSelected()},
           onCancel = {viewModel.onCancel()},
           multiProvider = uiState.providers.size > 1,
-          onMoreOptionsSelected = {viewModel.onMoreOptionsSelected(it)}
+          onMoreOptionsSelected = {viewModel.onMoreOptionsSelected()}
         )
         CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
-            providerInfo = uiState.selectedProvider!!,
             providerList = uiState.providers,
-            onBackButtonSelected = {viewModel.onBackButtonSelected(it)},
+            onBackButtonSelected = {viewModel.onBackButtonSelected()},
             onOptionSelected = {viewModel.onMoreOptionsRowSelected(it)}
           )
         CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
+          providerInfo = uiState.activeEntry?.activeProvider!!,
+          onDefaultOrNotSelected = {viewModel.onDefaultOrNotSelected()}
         )
       }
     },
@@ -216,10 +218,9 @@
 @ExperimentalMaterialApi
 @Composable
 fun MoreOptionsSelectionCard(
-  providerInfo: ProviderInfo,
   providerList: List<ProviderInfo>,
-  onBackButtonSelected: (String) -> Unit,
-  onOptionSelected: (String) -> Unit
+  onBackButtonSelected: () -> Unit,
+  onOptionSelected: (ActiveEntry) -> Unit
 ) {
   Card(
     backgroundColor = lightBackgroundColor,
@@ -233,7 +234,7 @@
         elevation = 0.dp,
         navigationIcon =
         {
-          IconButton(onClick = { onBackButtonSelected(providerInfo.name) }) {
+          IconButton(onClick = onBackButtonSelected) {
             Icon(Icons.Filled.ArrowBack, "backIcon"
             )
           }
@@ -262,9 +263,12 @@
           providerList.forEach { providerInfo ->
             providerInfo.createOptions.forEach { createOptionInfo ->
               item {
-                MoreOptionsInfoRow(providerInfo = providerInfo,
+                MoreOptionsInfoRow(
+                  providerInfo = providerInfo,
                   createOptionInfo = createOptionInfo,
-                  onOptionSelected = onOptionSelected)
+                  onOptionSelected = {
+                    onOptionSelected(ActiveEntry(providerInfo, createOptionInfo))
+                  })
               }
             }
           }
@@ -282,10 +286,37 @@
 @ExperimentalMaterialApi
 @Composable
 fun MoreOptionsRowIntroCard(
+  providerInfo: ProviderInfo,
+  onDefaultOrNotSelected: () -> Unit,
 ) {
   Card(
     backgroundColor = lightBackgroundColor,
   ) {
+    Column() {
+      Text(
+        text = stringResource(R.string.use_provider_for_all_title, providerInfo.name),
+        style = Typography.subtitle1,
+        modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
+      )
+      Row(
+        horizontalArrangement = Arrangement.SpaceBetween,
+        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+      ) {
+        CancelButton(
+          stringResource(R.string.use_once),
+          onclick = onDefaultOrNotSelected
+        )
+        ConfirmButton(
+          stringResource(R.string.set_as_default),
+          onclick = onDefaultOrNotSelected
+        )
+      }
+      Divider(
+        thickness = 18.dp,
+        color = Color.Transparent,
+        modifier = Modifier.padding(bottom = 40.dp)
+      )
+    }
   }
 }
 
@@ -359,11 +390,12 @@
 @ExperimentalMaterialApi
 @Composable
 fun CreationSelectionCard(
+  requestDisplayInfo: RequestDisplayInfo,
   providerInfo: ProviderInfo,
-  onOptionSelected: (Int) -> Unit,
+  onOptionSelected: () -> Unit,
   onCancel: () -> Unit,
   multiProvider: Boolean,
-  onMoreOptionsSelected: (String) -> Unit,
+  onMoreOptionsSelected: () -> Unit,
 ) {
   Card(
     backgroundColor = lightBackgroundColor,
@@ -398,14 +430,13 @@
         LazyColumn(
           verticalArrangement = Arrangement.spacedBy(2.dp)
         ) {
-          providerInfo.createOptions.forEach {
             item {
-              CreateOptionRow(createOptionInfo = it, onOptionSelected = onOptionSelected)
+              PrimaryCreateOptionRow(requestDisplayInfo = requestDisplayInfo,
+                onOptionSelected = onOptionSelected)
             }
-          }
           if (multiProvider) {
             item {
-              MoreOptionsRow(onSelect = { onMoreOptionsSelected(providerInfo.name) })
+              MoreOptionsRow(onSelect = onMoreOptionsSelected)
             }
           }
         }
@@ -465,14 +496,45 @@
 
 @ExperimentalMaterialApi
 @Composable
+fun PrimaryCreateOptionRow(
+  requestDisplayInfo: RequestDisplayInfo,
+  onOptionSelected: () -> Unit
+) {
+  Chip(
+    modifier = Modifier.fillMaxWidth(),
+    onClick = {onOptionSelected()},
+    // TODO: Add an icon generated by provider according to requestDisplayInfo type
+    colors = ChipDefaults.chipColors(
+      backgroundColor = Grey100,
+      leadingIconContentColor = Grey100
+    ),
+    shape = Shapes.large
+  ) {
+    Column() {
+      Text(
+        text = requestDisplayInfo.userName,
+        style = Typography.h6,
+        modifier = Modifier.padding(top = 16.dp)
+      )
+      Text(
+        text = requestDisplayInfo.displayName,
+        style = Typography.body2,
+        modifier = Modifier.padding(bottom = 16.dp)
+      )
+    }
+  }
+}
+
+@ExperimentalMaterialApi
+@Composable
 fun MoreOptionsInfoRow(
   providerInfo: ProviderInfo,
   createOptionInfo: CreateOptionInfo,
-  onOptionSelected: (String) -> Unit
+  onOptionSelected: () -> Unit
 ) {
     Chip(
         modifier = Modifier.fillMaxWidth(),
-        onClick = { onOptionSelected(providerInfo.name) },
+        onClick = onOptionSelected,
         leadingIcon = {
             Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
                 bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(),
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
index 3cf81da..38486e2c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
@@ -30,7 +30,8 @@
 data class CreatePasskeyUiState(
   val providers: List<ProviderInfo>,
   val currentScreenState: CreateScreenState,
-  val selectedProvider: ProviderInfo? = null,
+  val requestDisplayInfo: RequestDisplayInfo,
+  val activeEntry: ActiveEntry? = null,
 )
 
 class CreatePasskeyViewModel(
@@ -56,7 +57,8 @@
     } else if (uiState.providers.size == 1){
       uiState = uiState.copy(
         currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-        selectedProvider = uiState.providers.first()
+        activeEntry = ActiveEntry(uiState.providers.first(),
+          uiState.providers.first().createOptions.first())
       )
     } else {
       throw java.lang.IllegalStateException("Empty provider list.")
@@ -66,14 +68,15 @@
   fun onProviderSelected(providerName: String) {
     uiState = uiState.copy(
       currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-      selectedProvider = getProviderInfoByName(providerName)
+      activeEntry = ActiveEntry(getProviderInfoByName(providerName),
+        getProviderInfoByName(providerName).createOptions.first())
     )
   }
 
   fun onCreateOptionSelected(createOptionId: Int) {
     Log.d("Account Selector", "Option selected for creation: $createOptionId")
     CredentialManagerRepo.getInstance().onOptionSelected(
-      uiState.selectedProvider!!.name,
+      uiState.activeEntry?.activeProvider!!.name,
       createOptionId
     )
     dialogResult.value = DialogResult(
@@ -87,24 +90,22 @@
     }
   }
 
-  fun onMoreOptionsSelected(providerName: String) {
+  fun onMoreOptionsSelected() {
     uiState = uiState.copy(
-        currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
-        selectedProvider = getProviderInfoByName(providerName)
+      currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
     )
   }
 
-  fun onBackButtonSelected(providerName: String) {
+  fun onBackButtonSelected() {
     uiState = uiState.copy(
         currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-        selectedProvider = getProviderInfoByName(providerName)
     )
   }
 
-  fun onMoreOptionsRowSelected(providerName: String) {
+  fun onMoreOptionsRowSelected(activeEntry: ActiveEntry) {
     uiState = uiState.copy(
       currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO,
-      selectedProvider = getProviderInfoByName(providerName)
+      activeEntry = activeEntry
     )
   }
 
@@ -112,4 +113,25 @@
     CredentialManagerRepo.getInstance().onCancel()
     dialogResult.value = DialogResult(ResultState.CANCELED)
   }
+
+  fun onDefaultOrNotSelected() {
+    uiState = uiState.copy(
+      currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+    )
+    // TODO: implement the if choose as default or not logic later
+  }
+
+  fun onPrimaryCreateOptionInfoSelected() {
+    var createOptionId = uiState.activeEntry?.activeCreateOptionInfo?.id
+    Log.d("Account Selector", "Option selected for creation: $createOptionId")
+    if (createOptionId != null) {
+      CredentialManagerRepo.getInstance().onOptionSelected(
+        uiState.activeEntry?.activeProvider!!.name,
+        createOptionId
+      )
+    }
+    dialogResult.value = DialogResult(
+      ResultState.COMPLETE,
+    )
+  }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt
new file mode 100644
index 0000000..d6f1b5f
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a credential entry used during the get credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+abstract class CredentialEntryUi(
+  val credentialTypeIcon: Icon,
+  val profileIcon: Icon?,
+  val lastUsedTimeMillis: Long?,
+  val note: CharSequence?,
+) {
+  companion object {
+    fun fromSlice(slice: Slice): CredentialEntryUi {
+      return when (slice.spec?.type) {
+        TYPE_PUBLIC_KEY_CREDENTIAL -> PasskeyCredentialEntryUi.fromSlice(slice)
+        TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntryUi.fromSlice(slice)
+        else -> throw IllegalArgumentException("Unexpected type: ${slice.spec?.type}")
+      }
+    }
+
+    const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
+      "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
+    const val TYPE_PASSWORD_CREDENTIAL: String = "androidx.credentials.TYPE_PASSWORD"
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt
new file mode 100644
index 0000000..bb3b206
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+class PasskeyCredentialEntryUi(
+  val userName: CharSequence,
+  val userDisplayName: CharSequence?,
+  credentialTypeIcon: Icon,
+  profileIcon: Icon?,
+  lastUsedTimeMillis: Long?,
+  note: CharSequence?,
+) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) {
+  companion object {
+    fun fromSlice(slice: Slice): CredentialEntryUi {
+      var userName: CharSequence? = null
+      var userDisplayName: CharSequence? = null
+      var credentialTypeIcon: Icon? = null
+      var profileIcon: Icon? = null
+      var lastUsedTimeMillis: Long? = null
+      var note: CharSequence? = null
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_USER_NAME)) {
+          userName = it.text
+        } else if (it.hasHint(Entry.HINT_PASSKEY_USER_DISPLAY_NAME)) {
+          userDisplayName = it.text
+        } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
+          credentialTypeIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+          profileIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+          lastUsedTimeMillis = it.long
+        } else if (it.hasHint(Entry.HINT_NOTE)) {
+          note = it.text
+        }
+      }
+      // TODO: fail NPE more elegantly.
+      return PasskeyCredentialEntryUi(
+        userName!!, userDisplayName, credentialTypeIcon!!,
+        profileIcon, lastUsedTimeMillis, note,
+      )
+    }
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt
new file mode 100644
index 0000000..7311b70
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a password credential entry used during the get credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class PasswordCredentialEntryUi(
+  val userName: CharSequence,
+  val password: CharSequence,
+  credentialTypeIcon: Icon,
+  profileIcon: Icon?,
+  lastUsedTimeMillis: Long?,
+  note: CharSequence?,
+) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) {
+  companion object {
+    fun fromSlice(slice: Slice): CredentialEntryUi {
+      var userName: CharSequence? = null
+      var password: CharSequence? = null
+      var credentialTypeIcon: Icon? = null
+      var profileIcon: Icon? = null
+      var lastUsedTimeMillis: Long? = null
+      var note: CharSequence? = null
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_USER_NAME)) {
+          userName = it.text
+        } else if (it.hasHint(Entry.HINT_PASSWORD_VALUE)) {
+          password = it.text
+        } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
+          credentialTypeIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+          profileIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+          lastUsedTimeMillis = it.long
+        } else if (it.hasHint(Entry.HINT_NOTE)) {
+          note = it.text
+        }
+      }
+      // TODO: fail NPE more elegantly.
+      return PasswordCredentialEntryUi(
+        userName!!, password!!, credentialTypeIcon!!,
+        profileIcon, lastUsedTimeMillis, note,
+      )
+    }
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt
new file mode 100644
index 0000000..fad3309
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a save entry used during the create credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class SaveEntryUi(
+  val userProviderAccountName: CharSequence,
+  val credentialTypeIcon: Icon?,
+  val profileIcon: Icon?,
+  val passwordCount: Int?,
+  val passkeyCount: Int?,
+  val totalCredentialCount: Int?,
+  val lastUsedTimeMillis: Long?,
+) {
+  companion object {
+    fun fromSlice(slice: Slice): SaveEntryUi {
+      var userProviderAccountName: CharSequence? = null
+      var credentialTypeIcon: Icon? = null
+      var profileIcon: Icon? = null
+      var passwordCount: Int? = null
+      var passkeyCount: Int? = null
+      var totalCredentialCount: Int? = null
+      var lastUsedTimeMillis: Long? = null
+
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME)) {
+          userProviderAccountName = it.text
+        } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
+          credentialTypeIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+          profileIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PASSWORD_COUNT)) {
+          passwordCount = it.int
+        } else if (it.hasHint(Entry.HINT_PASSKEY_COUNT)) {
+          passkeyCount = it.int
+        } else if (it.hasHint(Entry.HINT_TOTAL_CREDENTIAL_COUNT)) {
+          totalCredentialCount = it.int
+        } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+          lastUsedTimeMillis = it.long
+        }
+      }
+      // TODO: fail NPE more elegantly.
+      return SaveEntryUi(
+        userProviderAccountName!!, credentialTypeIcon, profileIcon,
+        passwordCount, passkeyCount, totalCredentialCount, lastUsedTimeMillis,
+      )
+    }
+  }
+}
diff --git a/packages/SettingsLib/ActivityEmbedding/Android.bp b/packages/SettingsLib/ActivityEmbedding/Android.bp
index 332bebf..c35fb3b 100644
--- a/packages/SettingsLib/ActivityEmbedding/Android.bp
+++ b/packages/SettingsLib/ActivityEmbedding/Android.bp
@@ -26,4 +26,9 @@
         "androidx.window.extensions",
         "androidx.window.sidecar",
     ],
+
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.permission",
+    ],
 }
diff --git a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
index 2742558..0949e1d 100644
--- a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
+++ b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
@@ -21,6 +21,7 @@
     <uses-sdk android:minSdkVersion="21" />
 
     <application>
+        <uses-library android:name="org.apache.http.legacy" android:required="false" />
         <uses-library android:name="androidx.window.extensions" android:required="false" />
         <uses-library android:name="androidx.window.sidecar" android:required="false" />
     </application>
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 811cdd8..68c63da 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -17,6 +17,7 @@
 buildscript {
     ext {
         spa_min_sdk = 21
+        spa_target_sdk = 33
         jetpack_compose_version = '1.2.0-alpha04'
         jetpack_compose_compiler_version = '1.3.2'
         jetpack_compose_material3_version = '1.0.0-alpha06'
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 0a4972f..f1a24af 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.settingslib.spa.gallery">
 
+    <uses-sdk android:minSdkVersion="21"/>
+
     <application
         android:name=".GalleryApplication"
         android:icon="@mipmap/ic_launcher"
@@ -32,11 +34,6 @@
             </intent-filter>
         </activity>
 
-        <activity
-            android:name=".GalleryDebugActivity"
-            android:exported="true">
-        </activity>
-
         <provider
             android:name=".GalleryEntryProvider"
             android:authorities="com.android.spa.gallery.provider"
@@ -44,5 +41,20 @@
             android:exported="false">
         </provider>
 
+        <activity
+            android:name="com.android.settingslib.spa.framework.debug.BlankActivity"
+            android:exported="true">
+        </activity>
+        <activity
+            android:name="com.android.settingslib.spa.framework.debug.DebugActivity"
+            android:exported="true">
+        </activity>
+        <provider
+            android:name="com.android.settingslib.spa.framework.debug.DebugProvider"
+            android:authorities="com.android.spa.gallery.debug"
+            android:enabled="true"
+            android:exported="false">
+        </provider>
+
     </application>
 </manifest>
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
index 551a0b1..c1ce7d9 100644
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ b/packages/SettingsLib/Spa/gallery/build.gradle
@@ -21,12 +21,12 @@
 
 android {
     namespace 'com.android.settingslib.spa.gallery'
-    compileSdk 33
+    compileSdk spa_target_sdk
 
     defaultConfig {
         applicationId "com.android.settingslib.spa.gallery"
         minSdk spa_min_sdk
-        targetSdk 33
+        targetSdk spa_target_sdk
         versionCode 1
         versionName "1.0"
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index acb22da..4af2589 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -25,6 +25,7 @@
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
 import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
+import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
 import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
 import com.android.settingslib.spa.gallery.page.SliderPageProvider
 import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider
@@ -66,6 +67,7 @@
                 IllustrationPageProvider,
                 CategoryPageProvider,
                 ActionButtonPageProvider,
+                ProgressBarPageProvider,
             ),
             rootPages = listOf(
                 HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
index e40775a..7fd49db 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
@@ -31,6 +31,7 @@
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
 import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
+import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
 import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
 import com.android.settingslib.spa.gallery.page.SliderPageProvider
 import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider
@@ -54,6 +55,7 @@
             IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
         )
     }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
new file mode 100644
index 0000000..dc45df4
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.spa.gallery.page
+
+import android.os.Bundle
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.SystemUpdate
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.preference.ProgressBarPreference
+import com.android.settingslib.spa.widget.preference.ProgressBarPreferenceModel
+import com.android.settingslib.spa.widget.preference.ProgressBarWithDataPreference
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.CircularLoadingBar
+import com.android.settingslib.spa.widget.ui.CircularProgressBar
+import com.android.settingslib.spa.widget.ui.LinearLoadingBar
+import kotlinx.coroutines.delay
+
+private const val TITLE = "Sample ProgressBar"
+
+object ProgressBarPageProvider : SettingsPageProvider {
+    override val name = "ProgressBar"
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+            .setIsAllowSearch(true)
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        // Mocks a loading time of 2 seconds.
+        var loading by remember { mutableStateOf(true) }
+        LaunchedEffect(Unit) {
+            delay(2000)
+            loading = false
+        }
+
+        RegularScaffold(title = TITLE) {
+            // Auto update the progress and finally jump tp 0.4f.
+            var progress by remember { mutableStateOf(0f) }
+            LaunchedEffect(Unit) {
+                delay(2000)
+                while (progress < 1f) {
+                    delay(100)
+                    progress += 0.01f
+                }
+                delay(500)
+                progress = 0.4f
+            }
+
+            // Show as a placeholder for progress bar
+            LargeProgressBar(progress)
+            // The remaining information only shows after loading complete.
+            if (!loading) {
+                SimpleProgressBar()
+                ProgressBarWithData()
+                CircularProgressBar(progress = progress, radius = 160f)
+            }
+        }
+
+        // Add loading bar examples, running for 2 seconds.
+        LinearLoadingBar(isLoading = loading, yOffset = 64.dp)
+        CircularLoadingBar(isLoading = loading)
+    }
+}
+
+@Composable
+private fun LargeProgressBar(progress: Float) {
+    ProgressBarPreference(object : ProgressBarPreferenceModel {
+        override val title = "Large Progress Bar"
+        override val progress = progress
+        override val height = 20f
+    })
+}
+
+@Composable
+private fun SimpleProgressBar() {
+    ProgressBarPreference(object : ProgressBarPreferenceModel {
+        override val title = "Simple Progress Bar"
+        override val progress = 0.2f
+        override val icon = Icons.Outlined.SystemUpdate
+    })
+}
+
+@Composable
+private fun ProgressBarWithData() {
+    ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel {
+        override val title = "Progress Bar with Data"
+        override val progress = 0.2f
+        override val icon = Icons.Outlined.Delete
+    }, data = "25G")
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun ProgressBarPagePreview() {
+    SettingsTheme {
+        ProgressBarPageProvider.Page(null)
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/AndroidManifest.xml b/packages/SettingsLib/Spa/spa/AndroidManifest.xml
index 410bcdb..62800bd 100644
--- a/packages/SettingsLib/Spa/spa/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/spa/AndroidManifest.xml
@@ -14,4 +14,7 @@
   limitations under the License.
   -->
 
-<manifest package="com.android.settingslib.spa" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.spa">
+    <uses-sdk android:minSdkVersion="21"/>
+</manifest>
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 7e05e75..c587411 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -21,11 +21,11 @@
 
 android {
     namespace 'com.android.settingslib.spa'
-    compileSdk 33
+    compileSdk spa_target_sdk
 
     defaultConfig {
         minSdk spa_min_sdk
-        targetSdk 33
+        targetSdk spa_target_sdk
     }
 
     sourceSets {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
index 532f63b..d631708 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
@@ -16,21 +16,22 @@
 
 package com.android.settingslib.spa.framework
 
-import android.content.ComponentName
 import android.content.ContentProvider
 import android.content.ContentValues
 import android.content.Context
 import android.content.Intent
-import android.content.Intent.URI_INTENT_SCHEME
 import android.content.UriMatcher
 import android.content.pm.ProviderInfo
 import android.database.Cursor
 import android.database.MatrixCursor
 import android.net.Uri
 import android.util.Log
+import com.android.settingslib.spa.framework.common.ColumnEnum
+import com.android.settingslib.spa.framework.common.QueryEnum
 import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.addUri
+import com.android.settingslib.spa.framework.common.getColumns
 
 private const val TAG = "EntryProvider"
 
@@ -39,117 +40,15 @@
  * One can query the provider result by:
  *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
  * For gallery, AuthorityPath = com.android.spa.gallery.provider
- * For SettingsGoogle, AuthorityPath = com.android.settings.spa.provider
+ * For Settings, AuthorityPath = com.android.settings.spa.provider
  * Some examples:
- *   $ adb shell content query --uri content://<AuthorityPath>/page_debug
- *   $ adb shell content query --uri content://<AuthorityPath>/entry_debug
- *   $ adb shell content query --uri content://<AuthorityPath>/page_info
- *   $ adb shell content query --uri content://<AuthorityPath>/entry_info
  *   $ adb shell content query --uri content://<AuthorityPath>/search_sitemap
  *   $ adb shell content query --uri content://<AuthorityPath>/search_static
  *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
  */
 open class EntryProvider : ContentProvider() {
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
-
-    /**
-     * Enum to define all column names in provider.
-     */
-    enum class ColumnEnum(val id: String) {
-        // Columns related to page
-        PAGE_ID("pageId"),
-        PAGE_NAME("pageName"),
-        PAGE_ROUTE("pageRoute"),
-        PAGE_INTENT_URI("pageIntent"),
-        PAGE_ENTRY_COUNT("entryCount"),
-        HAS_RUNTIME_PARAM("hasRuntimeParam"),
-        PAGE_START_ADB("pageStartAdb"),
-
-        // Columns related to entry
-        ENTRY_ID("entryId"),
-        ENTRY_NAME("entryName"),
-        ENTRY_ROUTE("entryRoute"),
-        ENTRY_INTENT_URI("entryIntent"),
-        ENTRY_HIERARCHY_PATH("entryPath"),
-        ENTRY_START_ADB("entryStartAdb"),
-
-        // Columns related to search
-        ENTRY_TITLE("entryTitle"),
-        ENTRY_SEARCH_KEYWORD("entrySearchKw"),
-    }
-
-    /**
-     * Enum to define all queries supported in the provider.
-     */
-    enum class QueryEnum(
-        val queryPath: String,
-        val queryMatchCode: Int,
-        val columnNames: List<ColumnEnum>
-    ) {
-        // For debug
-        PAGE_DEBUG_QUERY(
-            "page_debug", 1,
-            listOf(ColumnEnum.PAGE_START_ADB)
-        ),
-        ENTRY_DEBUG_QUERY(
-            "entry_debug", 2,
-            listOf(ColumnEnum.ENTRY_START_ADB)
-        ),
-
-        // page related queries.
-        PAGE_INFO_QUERY(
-            "page_info", 100,
-            listOf(
-                ColumnEnum.PAGE_ID,
-                ColumnEnum.PAGE_NAME,
-                ColumnEnum.PAGE_ROUTE,
-                ColumnEnum.PAGE_INTENT_URI,
-                ColumnEnum.PAGE_ENTRY_COUNT,
-                ColumnEnum.HAS_RUNTIME_PARAM,
-            )
-        ),
-
-        // entry related queries
-        ENTRY_INFO_QUERY(
-            "entry_info", 200,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_NAME,
-                ColumnEnum.ENTRY_ROUTE,
-                ColumnEnum.ENTRY_INTENT_URI,
-            )
-        ),
-
-        // Search related queries
-        SEARCH_SITEMAP_QUERY(
-            "search_sitemap", 300,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_HIERARCHY_PATH,
-            )
-        ),
-        SEARCH_STATIC_DATA_QUERY(
-            "search_static", 301,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_TITLE,
-                ColumnEnum.ENTRY_SEARCH_KEYWORD,
-            )
-        ),
-        SEARCH_DYNAMIC_DATA_QUERY(
-            "search_dynamic", 302,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_TITLE,
-                ColumnEnum.ENTRY_SEARCH_KEYWORD,
-            )
-        ),
-    }
-
     private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
-    private fun addUri(authority: String, query: QueryEnum) {
-        uriMatcher.addURI(authority, query.queryPath, query.queryMatchCode)
-    }
 
     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
         TODO("Implement this to handle requests to delete one or more rows")
@@ -182,13 +81,9 @@
 
     override fun attachInfo(context: Context?, info: ProviderInfo?) {
         if (info != null) {
-            addUri(info.authority, QueryEnum.PAGE_DEBUG_QUERY)
-            addUri(info.authority, QueryEnum.ENTRY_DEBUG_QUERY)
-            addUri(info.authority, QueryEnum.PAGE_INFO_QUERY)
-            addUri(info.authority, QueryEnum.ENTRY_INFO_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_SITEMAP_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_STATIC_DATA_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_DYNAMIC_DATA_QUERY)
+            QueryEnum.SEARCH_SITEMAP_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_STATIC_DATA_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.addUri(uriMatcher, info.authority)
         }
         super.attachInfo(context, info)
     }
@@ -202,10 +97,6 @@
     ): Cursor? {
         return try {
             when (uriMatcher.match(uri)) {
-                QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug()
-                QueryEnum.ENTRY_DEBUG_QUERY.queryMatchCode -> queryEntryDebug()
-                QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
-                QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo()
                 QueryEnum.SEARCH_SITEMAP_QUERY.queryMatchCode -> querySearchSitemap()
                 QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData()
                 QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData()
@@ -219,73 +110,18 @@
         }
     }
 
-    private fun queryPageDebug(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns())
-        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
-            val command = createBrowsePageAdbCommand(pageWithEntry.page)
-            if (command != null) {
-                cursor.newRow().add(ColumnEnum.PAGE_START_ADB.id, command)
-            }
-        }
-        return cursor
-    }
-
-    private fun queryEntryDebug(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            val command = createBrowsePageAdbCommand(entry.containerPage(), entry.id)
-            if (command != null) {
-                cursor.newRow().add(ColumnEnum.ENTRY_START_ADB.id, command)
-            }
-        }
-        return cursor
-    }
-
-    private fun queryPageInfo(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns())
-        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
-            val page = pageWithEntry.page
-            cursor.newRow()
-                .add(ColumnEnum.PAGE_ID.id, page.id)
-                .add(ColumnEnum.PAGE_NAME.id, page.displayName)
-                .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
-                .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
-                .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
-                .add(
-                    ColumnEnum.PAGE_INTENT_URI.id,
-                    createBrowsePageIntent(page).toUri(URI_INTENT_SCHEME)
-                )
-        }
-        return cursor
-    }
-
-    private fun queryEntryInfo(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            cursor.newRow()
-                .add(ColumnEnum.ENTRY_ID.id, entry.id)
-                .add(ColumnEnum.ENTRY_NAME.id, entry.displayName)
-                .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute())
-                .add(
-                    ColumnEnum.ENTRY_INTENT_URI.id,
-                    createBrowsePageIntent(entry.containerPage(), entry.id).toUri(URI_INTENT_SCHEME)
-                )
-        }
-        return cursor
-    }
-
     private fun querySearchSitemap(): Cursor {
         val entryRepository by spaEnvironment.entryRepository
         val cursor = MatrixCursor(QueryEnum.SEARCH_SITEMAP_QUERY.getColumns())
         for (entry in entryRepository.getAllEntries()) {
             if (!entry.isAllowSearch) continue
+            val intent = entry.containerPage()
+                .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
+                ?: Intent()
             cursor.newRow()
                 .add(ColumnEnum.ENTRY_ID.id, entry.id)
                 .add(ColumnEnum.ENTRY_HIERARCHY_PATH.id, entryRepository.getEntryPath(entry.id))
+                .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME))
         }
         return cursor
     }
@@ -321,54 +157,4 @@
                 searchData?.keyword ?: emptyList<String>()
             )
     }
-
-    private fun createBrowsePageIntent(page: SettingsPage, entryId: String? = null): Intent {
-        if (!isPageBrowsable(page)) return Intent()
-        return Intent().setComponent(ComponentName(context!!, spaEnvironment.browseActivityClass!!))
-            .apply {
-                putExtra(BrowseActivity.KEY_DESTINATION, page.buildRoute())
-                if (entryId != null) {
-                    putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId)
-                }
-            }
-    }
-
-    private fun createBrowsePageAdbCommand(page: SettingsPage, entryId: String? = null): String? {
-        if (!isPageBrowsable(page)) return null
-        val packageName = context!!.packageName
-        val activityName = spaEnvironment.browseActivityClass!!.name.replace(packageName, "")
-        val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${page.buildRoute()}"
-        val highlightParam =
-            if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else ""
-        return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam"
-    }
-
-    private fun isPageBrowsable(page: SettingsPage): Boolean {
-        return context != null &&
-            spaEnvironment.browseActivityClass != null &&
-            !page.hasRuntimeParam()
-    }
-}
-
-fun EntryProvider.QueryEnum.getColumns(): Array<String> {
-    return columnNames.map { it.id }.toTypedArray()
-}
-
-fun EntryProvider.QueryEnum.getIndex(name: EntryProvider.ColumnEnum): Int {
-    return columnNames.indexOf(name)
-}
-
-fun Cursor.getString(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): String {
-    return this.getString(query.getIndex(columnName))
-}
-
-fun Cursor.getInt(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): Int {
-    return this.getInt(query.getIndex(columnName))
-}
-
-fun Cursor.getBoolean(
-    query: EntryProvider.QueryEnum,
-    columnName: EntryProvider.ColumnEnum
-): Boolean {
-    return this.getInt(query.getIndex(columnName)) == 1
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
new file mode 100644
index 0000000..0707429
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.spa.framework.common
+
+import android.content.UriMatcher
+
+/**
+ * Enum to define all column names in provider.
+ */
+enum class ColumnEnum(val id: String) {
+    // Columns related to page
+    PAGE_ID("pageId"),
+    PAGE_NAME("pageName"),
+    PAGE_ROUTE("pageRoute"),
+    PAGE_INTENT_URI("pageIntent"),
+    PAGE_ENTRY_COUNT("entryCount"),
+    HAS_RUNTIME_PARAM("hasRuntimeParam"),
+    PAGE_START_ADB("pageStartAdb"),
+
+    // Columns related to entry
+    ENTRY_ID("entryId"),
+    ENTRY_NAME("entryName"),
+    ENTRY_ROUTE("entryRoute"),
+    ENTRY_INTENT_URI("entryIntent"),
+    ENTRY_HIERARCHY_PATH("entryPath"),
+    ENTRY_START_ADB("entryStartAdb"),
+
+    // Columns related to search
+    ENTRY_TITLE("entryTitle"),
+    ENTRY_SEARCH_KEYWORD("entrySearchKw"),
+}
+
+/**
+ * Enum to define all queries supported in the provider.
+ */
+enum class QueryEnum(
+    val queryPath: String,
+    val queryMatchCode: Int,
+    val columnNames: List<ColumnEnum>
+) {
+    // For debug
+    PAGE_DEBUG_QUERY(
+        "page_debug", 1,
+        listOf(ColumnEnum.PAGE_START_ADB)
+    ),
+    ENTRY_DEBUG_QUERY(
+        "entry_debug", 2,
+        listOf(ColumnEnum.ENTRY_START_ADB)
+    ),
+
+    // page related queries.
+    PAGE_INFO_QUERY(
+        "page_info", 100,
+        listOf(
+            ColumnEnum.PAGE_ID,
+            ColumnEnum.PAGE_NAME,
+            ColumnEnum.PAGE_ROUTE,
+            ColumnEnum.PAGE_INTENT_URI,
+            ColumnEnum.PAGE_ENTRY_COUNT,
+            ColumnEnum.HAS_RUNTIME_PARAM,
+        )
+    ),
+
+    // entry related queries
+    ENTRY_INFO_QUERY(
+        "entry_info", 200,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_NAME,
+            ColumnEnum.ENTRY_ROUTE,
+            ColumnEnum.ENTRY_INTENT_URI,
+        )
+    ),
+
+    // Search related queries
+    SEARCH_SITEMAP_QUERY(
+        "search_sitemap", 300,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_HIERARCHY_PATH,
+            ColumnEnum.ENTRY_INTENT_URI,
+        )
+    ),
+    SEARCH_STATIC_DATA_QUERY(
+        "search_static", 301,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_TITLE,
+            ColumnEnum.ENTRY_SEARCH_KEYWORD,
+        )
+    ),
+    SEARCH_DYNAMIC_DATA_QUERY(
+        "search_dynamic", 302,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_TITLE,
+            ColumnEnum.ENTRY_SEARCH_KEYWORD,
+        )
+    ),
+}
+
+internal fun QueryEnum.getColumns(): Array<String> {
+    return columnNames.map { it.id }.toTypedArray()
+}
+
+internal fun QueryEnum.getIndex(name: ColumnEnum): Int {
+    return columnNames.indexOf(name)
+}
+
+internal fun QueryEnum.addUri(uriMatcher: UriMatcher, authority: String) {
+    uriMatcher.addURI(authority, queryPath, queryMatchCode)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 8616b9f..fb42f01 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -37,7 +37,7 @@
 }
 
 val LocalEntryDataProvider =
-    compositionLocalOf<EntryData> { object : EntryData{} }
+    compositionLocalOf<EntryData> { object : EntryData {} }
 
 /**
  * Defines data of a Settings entry.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index 8f63c47..07df96e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -16,8 +16,13 @@
 
 package com.android.settingslib.spa.framework.common
 
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
 import android.os.Bundle
 import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.BrowseActivity
 import com.android.settingslib.spa.framework.util.isRuntimeParam
 import com.android.settingslib.spa.framework.util.navLink
 import com.android.settingslib.spa.framework.util.normalize
@@ -111,6 +116,41 @@
             details = formatDisplayTitle()
         )
     }
+
+    fun createBrowseIntent(
+        context: Context?,
+        browseActivityClass: Class<out Activity>?,
+        entryId: String? = null
+    ): Intent? {
+        if (!isBrowsable(context, browseActivityClass)) return null
+        return Intent().setComponent(ComponentName(context!!, browseActivityClass!!))
+            .apply {
+                putExtra(BrowseActivity.KEY_DESTINATION, buildRoute())
+                if (entryId != null) {
+                    putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId)
+                }
+            }
+    }
+
+    fun createBrowseAdbCommand(
+        context: Context?,
+        browseActivityClass: Class<out Activity>?,
+        entryId: String? = null
+    ): String? {
+        if (!isBrowsable(context, browseActivityClass)) return null
+        val packageName = context!!.packageName
+        val activityName = browseActivityClass!!.name.replace(packageName, "")
+        val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${buildRoute()}"
+        val highlightParam =
+            if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else ""
+        return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam"
+    }
+
+    fun isBrowsable(context: Context?, browseActivityClass: Class<out Activity>?): Boolean {
+        return context != null &&
+            browseActivityClass != null &&
+            !hasRuntimeParam()
+    }
 }
 
 fun String.toHashId(): String {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
similarity index 73%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
index 6f96818..3015080 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
@@ -14,12 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.framework
+package com.android.settingslib.spa.framework.debug
 
-import android.content.Intent
-import android.net.Uri
 import android.os.Bundle
-import android.util.Log
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.material3.Text
@@ -33,8 +30,6 @@
 import androidx.navigation.compose.rememberNavController
 import androidx.navigation.navArgument
 import com.android.settingslib.spa.R
-import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION
-import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY
 import com.android.settingslib.spa.framework.common.LogCategory
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsPage
@@ -60,11 +55,10 @@
 /**
  * The Debug Activity to display all Spa Pages & Entries.
  * One can open the debug activity by:
- *   $ adb shell am start -n <Activity>
- * For gallery, Activity = com.android.settingslib.spa.gallery/.GalleryDebugActivity
- * For SettingsGoogle, Activity = com.android.settings/.spa.SpaDebugActivity
+ *   $ adb shell am start -n <Package>/com.android.settingslib.spa.framework.debug.DebugActivity
+ * For gallery, Package = com.android.settingslib.spa.gallery
  */
-open class DebugActivity : ComponentActivity() {
+class DebugActivity : ComponentActivity() {
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -79,30 +73,6 @@
         }
     }
 
-    private fun displayDebugMessage() {
-        val entryProviderAuthorities = spaEnvironment.entryProviderAuthorities ?: return
-
-        try {
-            val query = EntryProvider.QueryEnum.PAGE_INFO_QUERY
-            contentResolver.query(
-                Uri.parse("content://$entryProviderAuthorities/${query.queryPath}"),
-                null, null, null
-            ).use { cursor ->
-                while (cursor != null && cursor.moveToNext()) {
-                    val route = cursor.getString(query, EntryProvider.ColumnEnum.PAGE_ROUTE)
-                    val entryCount = cursor.getInt(query, EntryProvider.ColumnEnum.PAGE_ENTRY_COUNT)
-                    val hasRuntimeParam =
-                        cursor.getBoolean(query, EntryProvider.ColumnEnum.HAS_RUNTIME_PARAM)
-                    val message = "Page Info: $route ($entryCount) " +
-                        (if (hasRuntimeParam) "with" else "no") + "-runtime-params"
-                    spaEnvironment.logger.message(TAG, message, category = LogCategory.FRAMEWORK)
-                }
-            }
-        } catch (e: Exception) {
-            Log.e(TAG, "Provider querying exception:", e)
-        }
-    }
-
     @Composable
     private fun MainContent() {
         val navController = rememberNavController()
@@ -141,11 +111,6 @@
                 override val title = "List All Entries (${allEntry.size})"
                 override val onClick = navigator(route = ROUTE_All_ENTRIES)
             })
-            Preference(object : PreferenceModel {
-                override val title = "Query EntryProvider"
-                override val enabled = isEntryProviderAvailable().toState()
-                override val onClick = { displayDebugMessage() }
-            })
         }
     }
 
@@ -177,6 +142,7 @@
 
     @Composable
     fun OnePage(arguments: Bundle?) {
+        val context = LocalContext.current
         val entryRepository by spaEnvironment.entryRepository
         val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "")
         val pageWithEntry = entryRepository.getPageWithEntry(id)!!
@@ -186,7 +152,9 @@
             Text(text = "Entry size: ${pageWithEntry.entries.size}")
             Preference(model = object : PreferenceModel {
                 override val title = "open page"
-                override val enabled = isPageClickable(pageWithEntry.page).toState()
+                override val enabled =
+                    pageWithEntry.page.isBrowsable(context, spaEnvironment.browseActivityClass)
+                        .toState()
                 override val onClick = openPage(pageWithEntry.page)
             })
             EntryList(pageWithEntry.entries)
@@ -195,6 +163,7 @@
 
     @Composable
     fun OneEntry(arguments: Bundle?) {
+        val context = LocalContext.current
         val entryRepository by spaEnvironment.entryRepository
         val id = arguments!!.getString(PARAM_NAME_ENTRY_ID, "")
         val entry = entryRepository.getEntry(id)!!
@@ -202,7 +171,9 @@
         RegularScaffold(title = "Entry - ${entry.displayTitle()}") {
             Preference(model = object : PreferenceModel {
                 override val title = "open entry"
-                override val enabled = isEntryClickable(entry).toState()
+                override val enabled =
+                    entry.containerPage().isBrowsable(context, spaEnvironment.browseActivityClass)
+                        .toState()
                 override val onClick = openEntry(entry)
             })
             Text(text = entryContent)
@@ -223,12 +194,10 @@
 
     @Composable
     private fun openPage(page: SettingsPage): (() -> Unit)? {
-        if (!isPageClickable(page)) return null
         val context = LocalContext.current
+        val intent =
+            page.createBrowseIntent(context, spaEnvironment.browseActivityClass) ?: return null
         val route = page.buildRoute()
-        val intent = Intent(context, spaEnvironment.browseActivityClass).apply {
-            putExtra(KEY_DESTINATION, route)
-        }
         return {
             spaEnvironment.logger.message(
                 TAG, "OpenPage: $route", category = LogCategory.FRAMEWORK
@@ -239,13 +208,11 @@
 
     @Composable
     private fun openEntry(entry: SettingsEntry): (() -> Unit)? {
-        if (!isEntryClickable(entry)) return null
         val context = LocalContext.current
+        val intent = entry.containerPage()
+            .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
+            ?: return null
         val route = entry.containerPage().buildRoute()
-        val intent = Intent(context, spaEnvironment.browseActivityClass).apply {
-            putExtra(KEY_DESTINATION, route)
-            putExtra(KEY_HIGHLIGHT_ENTRY, entry.id)
-        }
         return {
             spaEnvironment.logger.message(
                 TAG, "OpenEntry: $route", category = LogCategory.FRAMEWORK
@@ -253,17 +220,9 @@
             context.startActivity(intent)
         }
     }
-
-    private fun isEntryProviderAvailable(): Boolean {
-        return spaEnvironment.entryProviderAuthorities != null
-    }
-
-    private fun isPageClickable(page: SettingsPage): Boolean {
-        return spaEnvironment.browseActivityClass != null && !page.hasRuntimeParam()
-    }
-
-    private fun isEntryClickable(entry: SettingsEntry): Boolean {
-        return spaEnvironment.browseActivityClass != null &&
-            !entry.containerPage().hasRuntimeParam()
-    }
 }
+
+/**
+ * A blank activity without any page.
+ */
+class BlankActivity : ComponentActivity()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
new file mode 100644
index 0000000..6c27109
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.spa.framework.debug
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.URI_INTENT_SCHEME
+import android.content.UriMatcher
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.ColumnEnum
+import com.android.settingslib.spa.framework.common.QueryEnum
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.addUri
+import com.android.settingslib.spa.framework.common.getColumns
+
+private const val TAG = "DebugProvider"
+
+/**
+ * The content provider to return debug data.
+ * One can query the provider result by:
+ *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
+ * For gallery, AuthorityPath = com.android.spa.gallery.debug
+ * Some examples:
+ *   $ adb shell content query --uri content://<AuthorityPath>/page_debug
+ *   $ adb shell content query --uri content://<AuthorityPath>/entry_debug
+ *   $ adb shell content query --uri content://<AuthorityPath>/page_info
+ *   $ adb shell content query --uri content://<AuthorityPath>/entry_info
+ */
+class DebugProvider : ContentProvider() {
+    private val spaEnvironment get() = SpaEnvironmentFactory.instance
+    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
+        TODO("Implement this to handle requests to delete one or more rows")
+    }
+
+    override fun getType(uri: Uri): String? {
+        TODO(
+            "Implement this to handle requests for the MIME type of the data" +
+                "at the given URI"
+        )
+    }
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? {
+        TODO("Implement this to handle requests to insert a new row.")
+    }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int {
+        TODO("Implement this to handle requests to update one or more rows.")
+    }
+
+    override fun onCreate(): Boolean {
+        Log.d(TAG, "onCreate")
+        return true
+    }
+
+    override fun attachInfo(context: Context?, info: ProviderInfo?) {
+        if (info != null) {
+            QueryEnum.PAGE_DEBUG_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.ENTRY_DEBUG_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.PAGE_INFO_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.ENTRY_INFO_QUERY.addUri(uriMatcher, info.authority)
+        }
+        super.attachInfo(context, info)
+    }
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor? {
+        return try {
+            when (uriMatcher.match(uri)) {
+                QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug()
+                QueryEnum.ENTRY_DEBUG_QUERY.queryMatchCode -> queryEntryDebug()
+                QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
+                QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo()
+                else -> throw UnsupportedOperationException("Unknown Uri $uri")
+            }
+        } catch (e: UnsupportedOperationException) {
+            throw e
+        } catch (e: Exception) {
+            Log.e(TAG, "Provider querying exception:", e)
+            null
+        }
+    }
+
+    private fun queryPageDebug(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns())
+        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
+            val command = pageWithEntry.page.createBrowseAdbCommand(
+                context,
+                spaEnvironment.browseActivityClass
+            )
+            if (command != null) {
+                cursor.newRow().add(ColumnEnum.PAGE_START_ADB.id, command)
+            }
+        }
+        return cursor
+    }
+
+    private fun queryEntryDebug(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            val command = entry.containerPage()
+                .createBrowseAdbCommand(context, spaEnvironment.browseActivityClass, entry.id)
+            if (command != null) {
+                cursor.newRow().add(ColumnEnum.ENTRY_START_ADB.id, command)
+            }
+        }
+        return cursor
+    }
+
+    private fun queryPageInfo(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns())
+        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
+            val page = pageWithEntry.page
+            val intent =
+                page.createBrowseIntent(context, spaEnvironment.browseActivityClass) ?: Intent()
+            cursor.newRow()
+                .add(ColumnEnum.PAGE_ID.id, page.id)
+                .add(ColumnEnum.PAGE_NAME.id, page.displayName)
+                .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
+                .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
+                .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
+                .add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
+        }
+        return cursor
+    }
+
+    private fun queryEntryInfo(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            val intent = entry.containerPage()
+                .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
+                ?: Intent()
+            cursor.newRow()
+                .add(ColumnEnum.ENTRY_ID.id, entry.id)
+                .add(ColumnEnum.ENTRY_NAME.id, entry.displayName)
+                .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute())
+                .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
+        }
+        return cursor
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
index 3fa8c65..52c4893 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
@@ -44,3 +44,6 @@
 
 val ColorScheme.divider: Color
     get() = onSurface.copy(SettingsOpacity.Divider)
+
+val ColorScheme.surfaceTone: Color
+    get() = primary.copy(SettingsOpacity.SurfaceTone)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
index 11af6ce..69ddf01 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
@@ -20,4 +20,5 @@
     const val Full = 1f
     const val Disabled = 0.38f
     const val Divider = 0.2f
+    const val SurfaceTone = 0.14f
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
index 6c7432e..8d0a35c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
@@ -23,7 +23,7 @@
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 
 @Composable
-fun LogEntryEvent(): (event: LogEvent) -> Unit {
+fun logEntryEvent(): (event: LogEvent) -> Unit {
     val entryId = LocalEntryDataProvider.current.entryId ?: return {}
     return {
         SpaEnvironmentFactory.instance.logger.event(entryId, it, category = LogCategory.VIEW)
@@ -31,9 +31,9 @@
 }
 
 @Composable
-fun WrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? {
+fun wrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? {
     if (onClick == null) return null
-    val logEvent = LogEntryEvent()
+    val logEvent = logEntryEvent()
     return {
         logEvent(LogEvent.ENTRY_CLICK)
         onClick()
@@ -41,9 +41,9 @@
 }
 
 @Composable
-fun WrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boolean) -> Unit)? {
+fun wrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boolean) -> Unit)? {
     if (onSwitch == null) return null
-    val logEvent = LogEntryEvent()
+    val logEvent = logEntryEvent()
     return {
         val event = if (it) LogEvent.ENTRY_SWITCH_ON else LogEvent.ENTRY_SWITCH_OFF
         logEvent(event)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
index 9a34dbf..6135203 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
@@ -72,7 +72,7 @@
 }
 
 @Composable
-private fun BaseIcon(
+internal fun BaseIcon(
     icon: @Composable (() -> Unit)?,
     modifier: Modifier,
     paddingStart: Dp,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
index 3e04b16..db95e23 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
@@ -28,7 +28,7 @@
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsShape
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 @Composable
 fun MainSwitchPreference(model: SwitchPreferenceModel) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
index 7c0116a..6ebe6bb 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
@@ -26,9 +26,9 @@
 import com.android.settingslib.spa.framework.common.EntrySearchData
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.compose.stateOf
-import com.android.settingslib.spa.framework.util.WrapOnClickWithLog
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.framework.util.wrapOnClickWithLog
 import com.android.settingslib.spa.widget.ui.createSettingsIcon
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 data class SimplePreferenceMacro(
     val title: String,
@@ -107,7 +107,7 @@
     model: PreferenceModel,
     singleLineSummary: Boolean = false,
 ) {
-    val onClickWithLog = WrapOnClickWithLog(model.onClick)
+    val onClickWithLog = wrapOnClickWithLog(model.onClick)
     val modifier = remember(model.enabled.value) {
         if (onClickWithLog != null) {
             Modifier.clickable(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt
new file mode 100644
index 0000000..b8c59ad
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt
@@ -0,0 +1,171 @@
+/*
+ * 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.spa.widget.preference
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.ui.LinearProgressBar
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+
+/**
+ * The widget model for [ProgressBarPreference] widget.
+ */
+interface ProgressBarPreferenceModel {
+    /**
+     * The title of this [ProgressBarPreference].
+     */
+    val title: String
+
+    /**
+     * The progress fraction of the ProgressBar. Should be float in range [0f, 1f]
+     */
+    val progress: Float
+
+    /**
+     * The icon image for [ProgressBarPreference]. If not specified, hides the icon by default.
+     */
+    val icon: ImageVector?
+        get() = null
+
+    /**
+     * The height of the ProgressBar.
+     */
+    val height: Float
+        get() = 4f
+
+    /**
+     * Indicates whether to use rounded corner for the progress bars.
+     */
+    val roundedCorner: Boolean
+        get() = true
+}
+
+/**
+ * Progress bar preference widget.
+ *
+ * Data is provided through [ProgressBarPreferenceModel].
+ */
+@Composable
+fun ProgressBarPreference(model: ProgressBarPreferenceModel) {
+    ProgressBarPreference(
+        title = model.title,
+        progress = model.progress,
+        icon = model.icon,
+        height = model.height,
+        roundedCorner = model.roundedCorner,
+    )
+}
+
+/**
+ * Progress bar with data preference widget.
+ */
+@Composable
+fun ProgressBarWithDataPreference(model: ProgressBarPreferenceModel, data: String) {
+    val icon = model.icon
+    ProgressBarWithDataPreference(
+        title = model.title,
+        data = data,
+        progress = model.progress,
+        icon = if (icon != null) ({
+            Icon(imageVector = icon, contentDescription = null)
+        }) else null,
+        height = model.height,
+        roundedCorner = model.roundedCorner,
+    )
+}
+
+@Composable
+internal fun ProgressBarPreference(
+    title: String,
+    progress: Float,
+    icon: ImageVector? = null,
+    height: Float = 4f,
+    roundedCorner: Boolean = true,
+) {
+    BaseLayout(
+        title = title,
+        subTitle = {
+            LinearProgressBar(progress, height, roundedCorner)
+        },
+        icon = if (icon != null) ({
+            Icon(imageVector = icon, contentDescription = null)
+        }) else null,
+    )
+}
+
+
+@Composable
+internal fun ProgressBarWithDataPreference(
+    title: String,
+    data: String,
+    progress: Float,
+    icon: (@Composable () -> Unit)? = null,
+    height: Float = 4f,
+    roundedCorner: Boolean = true,
+) {
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(end = SettingsDimension.itemPaddingEnd),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        BaseIcon(icon, Modifier, SettingsDimension.itemPaddingStart)
+        TitleWithData(
+            title = title,
+            data = data,
+            subTitle = {
+                LinearProgressBar(progress, height, roundedCorner)
+            },
+            modifier = Modifier
+                .weight(1f)
+                .padding(vertical = SettingsDimension.itemPaddingVertical),
+        )
+    }
+}
+
+@Composable
+private fun TitleWithData(
+    title: String,
+    data: String,
+    subTitle: @Composable () -> Unit,
+    modifier: Modifier
+) {
+    Column(modifier) {
+        Row {
+            Box(modifier = Modifier.weight(1f)) {
+                SettingsTitle(title)
+            }
+            Text(
+                text = data,
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                style = MaterialTheme.typography.titleMedium,
+            )
+        }
+        subTitle()
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
index 7bca38f..4ee2af0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
@@ -31,8 +31,8 @@
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.SettingsSlider
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 /**
  * The widget model for [SliderPreference] widget.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
index 592a99f..2d60619 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
@@ -31,9 +31,9 @@
 import com.android.settingslib.spa.framework.compose.toState
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog
 import com.android.settingslib.spa.widget.ui.SettingsSwitch
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 /**
  * The widget model for [SwitchPreference] widget.
@@ -104,7 +104,7 @@
 ) {
     val checkedValue = checked.value
     val indication = LocalIndication.current
-    val onChangeWithLog = WrapOnSwitchWithLog(onCheckedChange)
+    val onChangeWithLog = wrapOnSwitchWithLog(onCheckedChange)
     val modifier = remember(checkedValue, changeable.value) {
         if (checkedValue != null && onChangeWithLog != null) {
             Modifier.toggleable(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
index 63de2c8..fbfcaaa 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
@@ -17,7 +17,7 @@
 package com.android.settingslib.spa.widget.preference
 
 import androidx.compose.runtime.Composable
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.widget.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.SettingsSwitch
 
 @Composable
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt
new file mode 100644
index 0000000..1741f13
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.spa.widget.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.absoluteOffset
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Indeterminate linear progress bar. Expresses an unspecified wait time.
+ */
+@Composable
+fun LinearLoadingBar(
+    isLoading: Boolean,
+    xOffset: Dp = 0.dp,
+    yOffset: Dp = 0.dp
+) {
+    if (isLoading) {
+        LinearProgressIndicator(
+            modifier = Modifier
+                .fillMaxWidth()
+                .absoluteOffset(xOffset, yOffset)
+        )
+    }
+}
+
+/**
+ * Indeterminate circular progress bar. Expresses an unspecified wait time.
+ */
+@Composable
+fun CircularLoadingBar(isLoading: Boolean) {
+    if (isLoading) {
+        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+            CircularProgressIndicator()
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt
new file mode 100644
index 0000000..5d8502d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.spa.widget.ui
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.progressSemantics
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.unit.dp
+
+/**
+ * Determinate linear progress bar. Displays the current progress of the whole process.
+ *
+ * Rounded corner is supported and enabled by default.
+ */
+@Composable
+fun LinearProgressBar(
+    progress: Float,
+    height: Float = 4f,
+    roundedCorner: Boolean = true
+) {
+    Box(modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)) {
+        val color = MaterialTheme.colorScheme.onSurface
+        val trackColor = MaterialTheme.colorScheme.surfaceVariant
+        Canvas(
+            Modifier
+                .progressSemantics(progress)
+                .fillMaxWidth()
+                .height(height.dp)
+        ) {
+            drawLinearBarTrack(trackColor, roundedCorner)
+            drawLinearBar(progress, color, roundedCorner)
+        }
+    }
+}
+
+private fun DrawScope.drawLinearBar(
+    endFraction: Float,
+    color: Color,
+    roundedCorner: Boolean
+) {
+    val width = endFraction * size.width
+    drawRoundRect(
+        color = color,
+        size = Size(width, size.height),
+        cornerRadius = if (roundedCorner) CornerRadius(
+            size.height / 2,
+            size.height / 2
+        ) else CornerRadius.Zero,
+    )
+}
+
+private fun DrawScope.drawLinearBarTrack(
+    color: Color,
+    roundedCorner: Boolean
+) = drawLinearBar(1f, color, roundedCorner)
+
+/**
+ * Determinate circular progress bar. Displays the current progress of the whole process.
+ *
+ * Displayed in default material3 style, and rounded corner is not supported.
+ */
+@Composable
+fun CircularProgressBar(progress: Float, radius: Float = 40f) {
+    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+        CircularProgressIndicator(
+            progress = progress,
+            modifier = Modifier.size(radius.dp, radius.dp)
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
index d8455e4..48fec3b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
@@ -16,13 +16,16 @@
 
 package com.android.settingslib.spa.widget.ui
 
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.theme.surfaceTone
 import kotlin.math.roundToInt
 
 @Composable
@@ -45,5 +48,8 @@
         valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(),
         steps = if (showSteps) (valueRange.count() - 2) else 0,
         onValueChangeFinished = onValueChangeFinished,
+        colors = SliderDefaults.colors(
+            inactiveTrackColor = MaterialTheme.colorScheme.surfaceTone
+        )
     )
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
index 82ab0be..b969076 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
@@ -20,7 +20,7 @@
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
-import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog
+import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -35,7 +35,7 @@
     if (checkedValue != null) {
         Checkbox(
             checked = checkedValue,
-            onCheckedChange = WrapOnSwitchWithLog(onCheckedChange),
+            onCheckedChange = wrapOnSwitchWithLog(onCheckedChange),
             enabled = changeable.value,
         )
     } else {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
similarity index 96%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
index 8e24ce0..652e54d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.framework.util
+package com.android.settingslib.spa.widget.util
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp
index 1ce49fa..7491045 100644
--- a/packages/SettingsLib/Spa/tests/Android.bp
+++ b/packages/SettingsLib/Spa/tests/Android.bp
@@ -34,4 +34,5 @@
         "truth-prebuilt",
     ],
     kotlincflags: ["-Xjvm-default=all"],
+    min_sdk_version: "31",
 }
diff --git a/packages/SettingsLib/Spa/tests/AndroidManifest.xml b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
index c224caf..e2db594 100644
--- a/packages/SettingsLib/Spa/tests/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.settingslib.spa.tests">
 
+    <uses-sdk android:minSdkVersion="21"/>
+
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
index f950e01..b43bf18 100644
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -21,11 +21,11 @@
 
 android {
     namespace 'com.android.settingslib.spa.tests'
-    compileSdk 33
+    compileSdk spa_target_sdk
 
     defaultConfig {
         minSdk spa_min_sdk
-        targetSdk 33
+        targetSdk spa_target_sdk
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt
new file mode 100644
index 0000000..5611f8c
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.spa.widget.preference
+
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ProgressBarPreferenceTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun title_displayed() {
+        composeTestRule.setContent {
+            ProgressBarPreference(object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            })
+        }
+        composeTestRule.onNodeWithText("Title").assertIsDisplayed()
+    }
+
+    @Test
+    fun data_displayed() {
+        composeTestRule.setContent {
+            ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            }, data = "Data")
+        }
+        composeTestRule.onNodeWithText("Title").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Data").assertIsDisplayed()
+    }
+
+    @Test
+    fun progressBar_displayed() {
+        composeTestRule.setContent {
+            ProgressBarPreference(object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            })
+        }
+
+        fun progressEqualsTo(progress: Float): SemanticsMatcher =
+            SemanticsMatcher.expectValue(
+                ProgressBarRangeInfo,
+                ProgressBarRangeInfo(progress, 0f..1f, 0)
+            )
+        composeTestRule.onNode(progressEqualsTo(0.2f)).assertIsDisplayed()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt
index 7ae1175..3e5dd52 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt
@@ -16,6 +16,9 @@
 
 package com.android.settingslib.spa.widget.preference
 
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo
+import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
@@ -41,5 +44,20 @@
         composeTestRule.onNodeWithText("Slider").assertIsDisplayed()
     }
 
-    // TODO: Add more unit tests for SliderPreference widget.
+    @Test
+    fun slider_displayed() {
+        composeTestRule.setContent {
+            SliderPreference(object : SliderPreferenceModel {
+                override val title = "Slider"
+                override val initValue = 40
+            })
+        }
+
+        fun progressEqualsTo(progress: Float): SemanticsMatcher =
+            SemanticsMatcher.expectValue(
+                ProgressBarRangeInfo,
+                ProgressBarRangeInfo(progress, 0f..100f, 0)
+            )
+        composeTestRule.onNode(progressEqualsTo(40f)).assertIsDisplayed()
+    }
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
index 1dc52cb..9964926 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
@@ -3,6 +3,8 @@
 import android.app.admin.DevicePolicyManager
 import android.app.usage.StorageStatsManager
 import android.content.Context
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.os.UserHandle
 import android.os.UserManager
 
 /** The [UserManager] instance. */
@@ -13,3 +15,10 @@
 
 /** The [StorageStatsManager] instance. */
 val Context.storageStatsManager get() = getSystemService(StorageStatsManager::class.java)!!
+
+/** The [DomainVerificationManager] instance. */
+val Context.domainVerificationManager
+    get() = getSystemService(DomainVerificationManager::class.java)!!
+
+/** Gets a new [Context] for the given [UserHandle]. */
+fun Context.asUser(userHandle: UserHandle): Context = createContextAsUser(userHandle, 0)
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
index 91b852a..6641db1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
@@ -235,7 +235,7 @@
     /**
      * @return whether high quality audio is enabled or not
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public boolean isHighQualityAudioEnabled(BluetoothDevice device) {
         BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
         if (bluetoothDevice == null) {
@@ -287,7 +287,7 @@
      * @param device to get codec label from
      * @return the label associated with the device codec
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public String getHighQualityAudioOptionLabel(BluetoothDevice device) {
         BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
         int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
index 39977df..f969a63 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
@@ -41,19 +41,19 @@
         MobileNetworkTypeIcon icon =
                 MobileNetworkTypeIcons.getNetworkTypeIcon(TelephonyIcons.FOUR_G);
 
-        assertThat(icon.getName()).isEqualTo(TelephonyIcons.H_PLUS.name);
+        assertThat(icon.getName()).isEqualTo(TelephonyIcons.FOUR_G.name);
         assertThat(icon.getIconResId()).isEqualTo(TelephonyIcons.ICON_4G);
     }
 
     @Test
     public void getNetworkTypeIcon_unknown_returnsUnknown() {
-        SignalIcon.MobileIconGroup unknownGroup =
-                new SignalIcon.MobileIconGroup("testUnknownNameHere", 45, 6);
+        SignalIcon.MobileIconGroup unknownGroup = new SignalIcon.MobileIconGroup(
+                "testUnknownNameHere", /* dataContentDesc= */ 45, /* dataType= */ 6);
 
         MobileNetworkTypeIcon icon = MobileNetworkTypeIcons.getNetworkTypeIcon(unknownGroup);
 
         assertThat(icon.getName()).isEqualTo("testUnknownNameHere");
-        assertThat(icon.getIconResId()).isEqualTo(45);
-        assertThat(icon.getContentDescriptionResId()).isEqualTo(6);
+        assertThat(icon.getIconResId()).isEqualTo(6);
+        assertThat(icon.getContentDescriptionResId()).isEqualTo(45);
     }
 }
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index 3623c78..edea3ab 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -311,4 +311,7 @@
 
     <!-- Whether tilt to bright is enabled by default. -->
     <bool name="def_wearable_tiltToBrightEnabled">false</bool>
+
+    <!-- Whether vibrate icon is shown in the status bar by default. -->
+    <integer name="def_statusBarVibrateIconEnabled">0</integer>
 </resources>
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 3a25d85..ccbfac2 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3659,7 +3659,7 @@
         }
 
         private final class UpgradeController {
-            private static final int SETTINGS_VERSION = 210;
+            private static final int SETTINGS_VERSION = 211;
 
             private final int mUserId;
 
@@ -5531,7 +5531,17 @@
                     // removed now that feature is enabled for everyone
                     currentVersion = 210;
                 }
-
+                if (currentVersion == 210) {
+                    final SettingsState secureSettings = getSecureSettingsLocked(userId);
+                    final int defaultValueVibrateIconEnabled = getContext().getResources()
+                            .getInteger(R.integer.def_statusBarVibrateIconEnabled);
+                    secureSettings.insertSettingOverrideableByRestoreLocked(
+                            Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
+                            String.valueOf(defaultValueVibrateIconEnabled),
+                            null /* tag */, true /* makeDefault */,
+                            SettingsState.SYSTEM_PACKAGE_NAME);
+                    currentVersion = 211;
+                }
                 // vXXX: Add new settings above this point.
 
                 if (currentVersion != newVersion) {
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 9747a6c..aea2f52 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -817,7 +817,8 @@
                  Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
                  Settings.Secure.ACCESSIBILITY_SHOW_WINDOW_MAGNIFICATION_PROMPT,
                  Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
-                 Settings.Secure.UI_TRANSLATION_ENABLED);
+                 Settings.Secure.UI_TRANSLATION_ENABLED,
+                 Settings.Secure.CREDENTIAL_SERVICE);
 
     @Test
     public void systemSettingsBackedUpOrDenied() {
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index fecf124..90fab08 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -154,6 +154,7 @@
     <uses-permission android:name="android.permission.CONTROL_UI_TRACING" />
     <uses-permission android:name="android.permission.SIGNAL_PERSISTENT_PROCESSES" />
     <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
+    <uses-permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" />
     <!-- Internal permissions granted to the shell. -->
     <uses-permission android:name="android.permission.FORCE_BACK" />
     <uses-permission android:name="android.permission.BATTERY_STATS" />
@@ -210,6 +211,7 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.CREATE_USERS" />
+    <uses-permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION" />
     <uses-permission android:name="android.permission.QUERY_USERS" />
     <uses-permission android:name="android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP" />
     <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
@@ -715,6 +717,9 @@
     <!-- Permission required for CTS test - ActivityPermissionRationaleTest -->
     <uses-permission android:name="android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY" />
 
+    <!-- Permission required for CTS test - CtsDeviceLockTestCases -->
+    <uses-permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" />
+
     <application android:label="@string/app_label"
                 android:theme="@android:style/Theme.DeviceDefault.DayNight"
                 android:defaultToDeviceProtectedStorage="true"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index ca36fa4..fdfad2b 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -25,7 +25,6 @@
 import android.os.Looper
 import android.util.Log
 import android.util.MathUtils
-import android.view.GhostView
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@@ -86,6 +85,9 @@
          */
         val sourceIdentity: Any
 
+        /** The CUJ associated to this controller. */
+        val cuj: DialogCuj?
+
         /**
          * Move the drawing of the source in the overlay of [viewGroup].
          *
@@ -142,7 +144,31 @@
          * controlled by this controller.
          */
         // TODO(b/252723237): Make this non-nullable
-        fun jankConfigurationBuilder(cuj: Int): InteractionJankMonitor.Configuration.Builder?
+        fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder?
+
+        companion object {
+            /**
+             * Create a [Controller] that can animate [source] to and from a dialog.
+             *
+             * Important: The view must be attached to a [ViewGroup] when calling this function and
+             * during the animation. For safety, this method will return null when it is not.
+             *
+             * Note: The background of [view] should be a (rounded) rectangle so that it can be
+             * properly animated.
+             */
+            fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
+                if (source.parent !is ViewGroup) {
+                    Log.e(
+                        TAG,
+                        "Skipping animation as view $source is not attached to a ViewGroup",
+                        Exception(),
+                    )
+                    return null
+                }
+
+                return ViewDialogLaunchAnimatorController(source, cuj)
+            }
+        }
     }
 
     /**
@@ -172,7 +198,12 @@
         cuj: DialogCuj? = null,
         animateBackgroundBoundsChange: Boolean = false
     ) {
-        show(dialog, createController(view), cuj, animateBackgroundBoundsChange)
+        val controller = Controller.fromView(view, cuj)
+        if (controller == null) {
+            dialog.show()
+        } else {
+            show(dialog, controller, animateBackgroundBoundsChange)
+        }
     }
 
     /**
@@ -187,10 +218,10 @@
      * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
      * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
      */
+    @JvmOverloads
     fun show(
         dialog: Dialog,
         controller: Controller,
-        cuj: DialogCuj? = null,
         animateBackgroundBoundsChange: Boolean = false
     ) {
         if (Looper.myLooper() != Looper.getMainLooper()) {
@@ -207,7 +238,10 @@
                 it.dialog.window.decorView.viewRootImpl == controller.viewRoot
             }
         val animateFrom =
-            animatedParent?.dialogContentWithBackground?.let { createController(it) } ?: controller
+            animatedParent?.dialogContentWithBackground?.let {
+                Controller.fromView(it, controller.cuj)
+            }
+                ?: controller
 
         if (animatedParent == null && animateFrom !is LaunchableView) {
             // Make sure the View we launch from implements LaunchableView to avoid visibility
@@ -244,96 +278,12 @@
                 animateBackgroundBoundsChange,
                 animatedParent,
                 isForTesting,
-                cuj,
             )
 
         openedDialogs.add(animatedDialog)
         animatedDialog.start()
     }
 
-    /** Create a [Controller] that can animate [source] to & from a dialog. */
-    private fun createController(source: View): Controller {
-        return object : Controller {
-            override val viewRoot: ViewRootImpl
-                get() = source.viewRootImpl
-
-            override val sourceIdentity: Any = source
-
-            override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
-                // Create a temporary ghost of the source (which will make it invisible) and add it
-                // to the host dialog.
-                GhostView.addGhost(source, viewGroup)
-
-                // The ghost of the source was just created, so the source is currently invisible.
-                // We need to make sure that it stays invisible as long as the dialog is shown or
-                // animating.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
-            }
-
-            override fun stopDrawingInOverlay() {
-                // Note: here we should remove the ghost from the overlay, but in practice this is
-                // already done by the launch controllers created below.
-
-                // Make sure we allow the source to change its visibility again.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-                source.visibility = View.VISIBLE
-            }
-
-            override fun createLaunchController(): LaunchAnimator.Controller {
-                val delegate = GhostedViewLaunchAnimatorController(source)
-                return object : LaunchAnimator.Controller by delegate {
-                    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
-                        // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
-                        // ghost (that ghosts only the source content, and not its background) will
-                        // be added right after this by the delegate and will be animated.
-                        GhostView.removeGhost(source)
-                        delegate.onLaunchAnimationStart(isExpandingFullyAbove)
-                    }
-
-                    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
-                        delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
-
-                        // We hide the source when the dialog is showing. We will make this view
-                        // visible again when dismissing the dialog. This does nothing if the source
-                        // implements [LaunchableView], as it's already INVISIBLE in that case.
-                        source.visibility = View.INVISIBLE
-                    }
-                }
-            }
-
-            override fun createExitController(): LaunchAnimator.Controller {
-                return GhostedViewLaunchAnimatorController(source)
-            }
-
-            override fun shouldAnimateExit(): Boolean {
-                // The source should be invisible by now, if it's not then something else changed
-                // its visibility and we probably don't want to run the animation.
-                if (source.visibility != View.INVISIBLE) {
-                    return false
-                }
-
-                return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
-            }
-
-            override fun onExitAnimationCancelled() {
-                // Make sure we allow the source to change its visibility again.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-
-                // If the view is invisible it's probably because of us, so we make it visible
-                // again.
-                if (source.visibility == View.INVISIBLE) {
-                    source.visibility = View.VISIBLE
-                }
-            }
-
-            override fun jankConfigurationBuilder(
-                cuj: Int
-            ): InteractionJankMonitor.Configuration.Builder? {
-                return InteractionJankMonitor.Configuration.Builder.withView(cuj, source)
-            }
-        }
-    }
-
     /**
      * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will
      * allow for dismissing the whole stack.
@@ -563,9 +513,6 @@
      * Whether synchronization should be disabled, which can be useful if we are running in a test.
      */
     private val forceDisableSynchronization: Boolean,
-
-    /** Interaction to which the dialog animation is associated. */
-    private val cuj: DialogCuj? = null
 ) {
     /**
      * The DecorView of this dialog window.
@@ -618,8 +565,9 @@
     private var hasInstrumentedJank = false
 
     fun start() {
+        val cuj = controller.cuj
         if (cuj != null) {
-            val config = controller.jankConfigurationBuilder(cuj.cujType)
+            val config = controller.jankConfigurationBuilder()
             if (config != null) {
                 if (cuj.tag != null) {
                     config.setTag(cuj.tag)
@@ -917,7 +865,7 @@
                 }
 
                 if (hasInstrumentedJank) {
-                    interactionJankMonitor.end(cuj!!.cujType)
+                    interactionJankMonitor.end(controller.cuj!!.cujType)
                 }
             }
         )
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
index 8ce372d..40a5e97 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
@@ -30,7 +30,12 @@
      */
     fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller?
 
-    // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here.
+    /**
+     * Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into
+     * a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is
+     * currently not attached or visible).
+     */
+    fun dialogLaunchController(cuj: DialogCuj? = null): DialogLaunchAnimator.Controller?
 
     companion object {
         /**
@@ -39,6 +44,7 @@
          * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
          * animated.
          */
+        @JvmStatic
         fun fromView(view: View): Expandable {
             return object : Expandable {
                 override fun activityLaunchController(
@@ -46,6 +52,12 @@
                 ): ActivityLaunchAnimator.Controller? {
                     return ActivityLaunchAnimator.Controller.fromView(view, cujType)
                 }
+
+                override fun dialogLaunchController(
+                    cuj: DialogCuj?
+                ): DialogLaunchAnimator.Controller? {
+                    return DialogLaunchAnimator.Controller.fromView(view, cuj)
+                }
             }
         }
     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
new file mode 100644
index 0000000..ecee598
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.animation
+
+import android.view.GhostView
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewRootImpl
+import com.android.internal.jank.InteractionJankMonitor
+
+/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */
+class ViewDialogLaunchAnimatorController
+internal constructor(
+    private val source: View,
+    override val cuj: DialogCuj?,
+) : DialogLaunchAnimator.Controller {
+    override val viewRoot: ViewRootImpl
+        get() = source.viewRootImpl
+
+    override val sourceIdentity: Any = source
+
+    override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
+        // Create a temporary ghost of the source (which will make it invisible) and add it
+        // to the host dialog.
+        GhostView.addGhost(source, viewGroup)
+
+        // The ghost of the source was just created, so the source is currently invisible.
+        // We need to make sure that it stays invisible as long as the dialog is shown or
+        // animating.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+    }
+
+    override fun stopDrawingInOverlay() {
+        // Note: here we should remove the ghost from the overlay, but in practice this is
+        // already done by the launch controllers created below.
+
+        // Make sure we allow the source to change its visibility again.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
+        source.visibility = View.VISIBLE
+    }
+
+    override fun createLaunchController(): LaunchAnimator.Controller {
+        val delegate = GhostedViewLaunchAnimatorController(source)
+        return object : LaunchAnimator.Controller by delegate {
+            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+                // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
+                // ghost (that ghosts only the source content, and not its background) will
+                // be added right after this by the delegate and will be animated.
+                GhostView.removeGhost(source)
+                delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+            }
+
+            override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+
+                // We hide the source when the dialog is showing. We will make this view
+                // visible again when dismissing the dialog. This does nothing if the source
+                // implements [LaunchableView], as it's already INVISIBLE in that case.
+                source.visibility = View.INVISIBLE
+            }
+        }
+    }
+
+    override fun createExitController(): LaunchAnimator.Controller {
+        return GhostedViewLaunchAnimatorController(source)
+    }
+
+    override fun shouldAnimateExit(): Boolean {
+        // The source should be invisible by now, if it's not then something else changed
+        // its visibility and we probably don't want to run the animation.
+        if (source.visibility != View.INVISIBLE) {
+            return false
+        }
+
+        return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
+    }
+
+    override fun onExitAnimationCancelled() {
+        // Make sure we allow the source to change its visibility again.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
+
+        // If the view is invisible it's probably because of us, so we make it visible
+        // again.
+        if (source.visibility == View.INVISIBLE) {
+            source.visibility = View.VISIBLE
+        }
+    }
+
+    override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
+        val type = cuj?.cujType ?: return null
+        return InteractionJankMonitor.Configuration.Builder.withView(type, source)
+    }
+}
diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp
index 9671add..40580d2 100644
--- a/packages/SystemUI/checks/Android.bp
+++ b/packages/SystemUI/checks/Android.bp
@@ -47,6 +47,10 @@
         "tests/**/*.kt",
         "tests/**/*.java",
     ],
+    data: [
+        ":framework",
+        ":androidx.annotation_annotation",
+    ],
     static_libs: [
         "SystemUILintChecker",
         "junit",
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
index 4eeeb85..4b9aa13 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
@@ -32,7 +32,8 @@
 class SoftwareBitmapDetector : Detector(), SourceCodeScanner {
 
     override fun getApplicableReferenceNames(): List<String> {
-        return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102")
+        return mutableListOf(
+            "ALPHA_8", "RGB_565", "ARGB_4444", "ARGB_8888", "RGBA_F16", "RGBA_1010102")
     }
 
     override fun visitReference(
@@ -40,13 +41,12 @@
             reference: UReferenceExpression,
             referenced: PsiElement
     ) {
-
         val evaluator = context.evaluator
         if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) {
             context.report(
                     ISSUE,
                     referenced,
-                    context.getNameLocation(referenced),
+                    context.getNameLocation(reference),
                     "Replace software bitmap with `Config.HARDWARE`"
             )
         }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
new file mode 100644
index 0000000..1db0725
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+private const val CLASS_SETTINGS = "android.provider.Settings"
+
+/**
+ * Detects usage of static methods in android.provider.Settings and suggests to use an injected
+ * settings provider instance instead.
+ */
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames(): List<String> {
+        return listOf(
+            "getFloat",
+            "getInt",
+            "getLong",
+            "getString",
+            "getUriFor",
+            "putFloat",
+            "putInt",
+            "putLong",
+            "putString"
+        )
+    }
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        val evaluator = context.evaluator
+        val className = method.containingClass?.qualifiedName
+        if (
+            className != "$CLASS_SETTINGS.Global" &&
+                className != "$CLASS_SETTINGS.Secure" &&
+                className != "$CLASS_SETTINGS.System"
+        ) {
+            return
+        }
+        if (!evaluator.isStatic(method)) {
+            return
+        }
+
+        val subclassName = className.substring(CLASS_SETTINGS.length + 1)
+
+        context.report(
+            ISSUE,
+            method,
+            context.getNameLocation(node),
+            "`@Inject` a ${subclassName}Settings instead"
+        )
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE: Issue =
+            Issue.create(
+                id = "StaticSettingsProvider",
+                briefDescription = "Static settings provider usage",
+                explanation =
+                    """
+                    Static settings provider methods, such as `Settings.Global.putInt()`, should \
+                    not be used because they make testing difficult. Instead, use an injected \
+                    settings provider. For example, instead of calling `Settings.Secure.getInt()`, \
+                    annotate the class constructor with `@Inject` and add `SecureSettings` to the \
+                    parameters.
+                    """,
+                category = Category.CORRECTNESS,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(
+                        StaticSettingsProviderDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                    )
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index cf7c1b5..3f334c1c 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -36,6 +36,7 @@
                 RegisterReceiverViaContextDetector.ISSUE,
                 SoftwareBitmapDetector.ISSUE,
                 NonInjectedServiceDetector.ISSUE,
+                StaticSettingsProviderDetector.ISSUE
         )
 
     override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index 486af9d..141dd05 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -18,6 +18,8 @@
 
 import com.android.annotations.NonNull
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile
+import java.io.File
 import org.intellij.lang.annotations.Language
 
 @Suppress("UnstableApiUsage")
@@ -30,132 +32,8 @@
  */
 internal val androidStubs =
     arrayOf(
-        indentedJava(
-            """
-package android.app;
-
-public class ActivityManager {
-    public static int getCurrentUser() {}
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.accounts;
-
-public class AccountManager {
-    public static AccountManager get(Context context) { return null; }
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.os;
-import android.content.pm.UserInfo;
-import android.annotation.UserIdInt;
-
-public class UserManager {
-    public UserInfo getUserInfo(@UserIdInt int userId) {}
-}
-"""
-        ),
-        indentedJava("""
-package android.annotation;
-
-public @interface UserIdInt {}
-"""),
-        indentedJava("""
-package android.content.pm;
-
-public class UserInfo {}
-"""),
-        indentedJava("""
-package android.os;
-
-public class Looper {}
-"""),
-        indentedJava("""
-package android.os;
-
-public class Handler {}
-"""),
-        indentedJava("""
-package android.content;
-
-public class ServiceConnection {}
-"""),
-        indentedJava("""
-package android.os;
-
-public enum UserHandle {
-    ALL
-}
-"""),
-        indentedJava(
-            """
-package android.content;
-import android.os.UserHandle;
-import android.os.Handler;
-import android.os.Looper;
-import java.util.concurrent.Executor;
-
-public class Context {
-    public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {}
-    public void registerReceiverAsUser(
-            BroadcastReceiver receiver, UserHandle user, IntentFilter filter,
-            String broadcastPermission, Handler scheduler) {}
-    public void registerReceiverForAllUsers(
-            BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission,
-            Handler scheduler) {}
-    public void sendBroadcast(Intent intent) {}
-    public void sendBroadcast(Intent intent, String receiverPermission) {}
-    public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {}
-    public void bindService(Intent intent) {}
-    public void bindServiceAsUser(
-            Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {}
-    public void unbindService(ServiceConnection connection) {}
-    public Looper getMainLooper() { return null; }
-    public Executor getMainExecutor() { return null; }
-    public Handler getMainThreadHandler() { return null; }
-    public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; }
-    public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name);
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.app;
-import android.content.Context;
-
-public class Activity extends Context {}
-"""
-        ),
-        indentedJava(
-            """
-package android.graphics;
-
-public class Bitmap {
-    public enum Config {
-        ARGB_8888,
-        RGB_565,
-        HARDWARE
-    }
-    public static Bitmap createBitmap(int width, int height, Config config) {
-        return null;
-    }
-}
-"""
-        ),
-        indentedJava("""
-package android.content;
-
-public class BroadcastReceiver {}
-"""),
-        indentedJava("""
-package android.content;
-
-public class IntentFilter {}
-"""),
+        LibraryReferenceTestFile(File("framework.jar").canonicalFile),
+        LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile),
         indentedJava(
             """
 package com.android.systemui.settings;
@@ -167,23 +45,4 @@
 }
 """
         ),
-        indentedJava(
-            """
-package androidx.annotation;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.CONSTRUCTOR;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-@Retention(SOURCE)
-@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
-public @interface WorkerThread {
-}
-"""
-        ),
     )
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
index 6ae8fd3..c35ac61 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class BindServiceOnMainThreadDetectorTest : LintDetectorTest() {
+class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = BindServiceOnMainThreadDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index 7d42280..376acb5 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class BroadcastSentViaContextDetectorTest : LintDetectorTest() {
+class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = BroadcastSentViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
index c468af8..301c338 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class NonInjectedMainThreadDetectorTest : LintDetectorTest() {
+class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = NonInjectedMainThreadDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
index c83a35b..0a74bfc 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class NonInjectedServiceDetectorTest : LintDetectorTest() {
+class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = NonInjectedServiceDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
     override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE)
 
     @Test
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index ebcddeb..9ed7aa0 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class RegisterReceiverViaContextDetectorTest : LintDetectorTest() {
+class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = RegisterReceiverViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index b03a11c..54cac7b 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class SlowUserQueryDetectorTest : LintDetectorTest() {
+class SlowUserQueryDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = SlowUserQueryDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> =
         listOf(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
index fb6537e..c632636 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class SoftwareBitmapDetectorTest : LintDetectorTest() {
+class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = SoftwareBitmapDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE)
 
@@ -54,12 +51,12 @@
             .run()
             .expect(
                 """
-                src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
-                        ARGB_8888,
-                        ~~~~~~~~~
-                src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
-                        RGB_565,
-                        ~~~~~~~
+                src/TestClass.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+                      Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565);
+                                                                  ~~~~~~~
+                src/TestClass.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+                      Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+                                                                  ~~~~~~~~~
                 0 errors, 2 warnings
                 """
             )
@@ -70,7 +67,7 @@
         lint()
             .files(
                 TestFiles.java(
-                        """
+                    """
                     import android.graphics.Bitmap;
 
                     public class TestClass {
@@ -79,8 +76,7 @@
                         }
                     }
                 """
-                    )
-                    .indented(),
+                ),
                 *stubs
             )
             .issues(SoftwareBitmapDetector.ISSUE)
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
new file mode 100644
index 0000000..b83ed70
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() {
+
+    override fun getDetector(): Detector = StaticSettingsProviderDetector()
+    override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE)
+
+    @Test
+    fun testGetServiceWithString() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+
+                        import android.provider.Settings;
+                        import android.provider.Settings.Global;
+                        import android.provider.Settings.Secure;
+
+                        public class TestClass {
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                final ContentResolver cr = mContext.getContentResolver();
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                                Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                                Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                                Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                                Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                                Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                                Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                                Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+
+                                Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                                Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                                Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                                Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                                Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                                Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                                Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                                Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+
+                                Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+                                Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(StaticSettingsProviderDetector.ISSUE)
+            .run()
+            .expect(
+                """
+                src/test/pkg/TestClass.java:10: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:11: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:12: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:13: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:14: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:15: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:16: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:17: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:18: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:19: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:20: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:21: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:23: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:24: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:25: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:26: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:27: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:28: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:29: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:30: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:31: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:32: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:33: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:34: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:36: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:37: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:38: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:39: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~~~
+                src/test/pkg/TestClass.java:40: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:41: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:42: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:43: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+                                        ~~~~~~~~~
+                src/test/pkg/TestClass.java:44: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:45: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:46: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:47: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                                        ~~~~~~~~~
+                0 errors, 36 warnings
+                """
+            )
+    }
+
+    private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
new file mode 100644
index 0000000..3f93f07
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
@@ -0,0 +1,48 @@
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import java.io.File
+import org.junit.ClassRule
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.model.Statement
+
+@Suppress("UnstableApiUsage")
+@RunWith(JUnit4::class)
+abstract class SystemUILintDetectorTest : LintDetectorTest() {
+
+    companion object {
+        @ClassRule
+        @JvmField
+        val libraryChecker: LibraryExists =
+            LibraryExists("framework.jar", "androidx.annotation_annotation.jar")
+    }
+
+    class LibraryExists(vararg val libraryNames: String) : TestRule {
+        override fun apply(base: Statement, description: Description): Statement {
+            return object : Statement() {
+                override fun evaluate() {
+                    for (libName in libraryNames) {
+                        val libFile = File(libName)
+                        if (!libFile.canonicalFile.exists()) {
+                            throw Exception(
+                                "Could not find $libName in the test's working directory. " +
+                                    "File ${libFile.absolutePath} does not exist."
+                            )
+                        }
+                    }
+                    base.evaluate()
+                }
+            }
+        }
+    }
+    /**
+     * Customize the lint task to disable SDK usage completely. This ensures that running the tests
+     * in Android Studio has the same result as running the tests in atest
+     */
+    override fun lint(): TestLintTask =
+        super.lint().allowMissingSdk(true).sdkHome(File("/dev/null"))
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
index 065c314..50c3d7e 100644
--- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
@@ -40,17 +40,16 @@
 import androidx.compose.ui.unit.LayoutDirection
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.animation.LaunchAnimator
 import kotlin.math.roundToInt
 
-/** A controller that can control animated launches. */
+/** A controller that can control animated launches from an [Expandable]. */
 interface ExpandableController {
-    /** Create an [ActivityLaunchAnimator.Controller] to animate into an Activity. */
-    fun forActivity(): ActivityLaunchAnimator.Controller
-
-    /** Create a [DialogLaunchAnimator.Controller] to animate into a Dialog. */
-    fun forDialog(): DialogLaunchAnimator.Controller
+    /** The [Expandable] controlled by this controller. */
+    val expandable: Expandable
 }
 
 /**
@@ -120,13 +119,26 @@
     private val layoutDirection: LayoutDirection,
     private val isComposed: State<Boolean>,
 ) : ExpandableController {
-    override fun forActivity(): ActivityLaunchAnimator.Controller {
-        return activityController()
-    }
+    override val expandable: Expandable =
+        object : Expandable {
+            override fun activityLaunchController(
+                cujType: Int?,
+            ): ActivityLaunchAnimator.Controller? {
+                if (!isComposed.value) {
+                    return null
+                }
 
-    override fun forDialog(): DialogLaunchAnimator.Controller {
-        return dialogController()
-    }
+                return activityController(cujType)
+            }
+
+            override fun dialogLaunchController(cuj: DialogCuj?): DialogLaunchAnimator.Controller? {
+                if (!isComposed.value) {
+                    return null
+                }
+
+                return dialogController(cuj)
+            }
+        }
 
     /**
      * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog
@@ -233,7 +245,7 @@
     }
 
     /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
-    private fun activityController(): ActivityLaunchAnimator.Controller {
+    private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller {
         val delegate = launchController()
         return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
             override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -248,10 +260,11 @@
         }
     }
 
-    private fun dialogController(): DialogLaunchAnimator.Controller {
+    private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller {
         return object : DialogLaunchAnimator.Controller {
             override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl
             override val sourceIdentity: Any = this@ExpandableControllerImpl
+            override val cuj: DialogCuj? = cuj
 
             override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
                 val newOverlay = viewGroup.overlay as ViewGroupOverlay
@@ -294,9 +307,7 @@
                 isDialogShowing.value = false
             }
 
-            override fun jankConfigurationBuilder(
-                cuj: Int
-            ): InteractionJankMonitor.Configuration.Builder? {
+            override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
                 // TODO(b/252723237): Add support for jank monitoring when animating from a
                 // Composable.
                 return null
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index d0d3052..31ab247 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -832,7 +832,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateLoggingProviderTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index b3dd955..dee0f5c 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -205,6 +205,13 @@
             n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)),
             n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666))
     )),
+    MONOCHROMATIC(CoreSpec(
+            a1 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            a2 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            a3 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            n1 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            n2 = TonalSpec(HueSource(), ChromaConstant(.0))
+    )),
 }
 
 class ColorScheme(
@@ -219,7 +226,7 @@
     val neutral1: List<Int>
     val neutral2: List<Int>
 
-    constructor(@ColorInt seed: Int, darkTheme: Boolean):
+    constructor(@ColorInt seed: Int, darkTheme: Boolean) :
             this(seed, darkTheme, Style.TONAL_SPOT)
 
     @JvmOverloads
@@ -227,7 +234,7 @@
         wallpaperColors: WallpaperColors,
         darkTheme: Boolean,
         style: Style = Style.TONAL_SPOT
-    ):
+    ) :
             this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style)
 
     val allAccentColors: List<Int>
@@ -472,4 +479,4 @@
             return huePopulation
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index cafaaf8..7709f21 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -33,6 +33,7 @@
 
     static_libs: [
         "androidx.annotation_annotation",
+        "error_prone_annotations",
         "PluginCoreLib",
         "SystemUIAnimationLib",
     ],
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
index 6124e10..6436dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
@@ -14,12 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
 import android.os.Trace
 import android.util.Log
-import com.android.systemui.log.dagger.LogModule
-import com.android.systemui.util.collection.RingBuffer
+import com.android.systemui.plugins.util.RingBuffer
 import com.google.errorprone.annotations.CompileTimeConstant
 import java.io.PrintWriter
 import java.util.concurrent.ArrayBlockingQueue
@@ -61,15 +60,18 @@
  * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or
  * the first letter of any of the previous.
  *
- * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory].
+ * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI
+ * LogBufferFactory.
  *
  * @param name The name of this buffer, printed when the buffer is dumped and in some other
  * situations.
  * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
- * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches
- * the maximum, it behaves like a ring buffer.
+ * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches the
+ * maximum, it behaves like a ring buffer.
  */
-class LogBuffer @JvmOverloads constructor(
+class LogBuffer
+@JvmOverloads
+constructor(
     private val name: String,
     private val maxSize: Int,
     private val logcatEchoTracker: LogcatEchoTracker,
@@ -78,7 +80,7 @@
     private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
 
     private val echoMessageQueue: BlockingQueue<LogMessage>? =
-            if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
+        if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
 
     init {
         if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
@@ -133,11 +135,11 @@
      */
     @JvmOverloads
     inline fun log(
-            tag: String,
-            level: LogLevel,
-            messageInitializer: MessageInitializer,
-            noinline messagePrinter: MessagePrinter,
-            exception: Throwable? = null,
+        tag: String,
+        level: LogLevel,
+        messageInitializer: MessageInitializer,
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
     ) {
         val message = obtain(tag, level, messagePrinter, exception)
         messageInitializer(message)
@@ -152,14 +154,13 @@
      * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in
      * an initializer and a message printer.
      *
-     * Log buffers are limited by the number of entries, so logging more frequently
-     * will limit the time window that the LogBuffer covers in a bug report.  Richer logs, on the
-     * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to
-     * add more detail to every log may do more to improve overall logging than adding more logs
-     * with this method.
+     * Log buffers are limited by the number of entries, so logging more frequently will limit the
+     * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a
+     * bug report more actionable, so using the [log] with a messagePrinter to add more detail to
+     * every log may do more to improve overall logging than adding more logs with this method.
      */
     fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
-            log(tag, level, {str1 = message}, { str1!! })
+        log(tag, level, { str1 = message }, { str1!! })
 
     /**
      * You should call [log] instead of this method.
@@ -172,10 +173,10 @@
      */
     @Synchronized
     fun obtain(
-            tag: String,
-            level: LogLevel,
-            messagePrinter: MessagePrinter,
-            exception: Throwable? = null,
+        tag: String,
+        level: LogLevel,
+        messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
     ): LogMessage {
         if (!mutable) {
             return FROZEN_MESSAGE
@@ -189,8 +190,7 @@
      * You should call [log] instead of this method.
      *
      * After acquiring a message via [obtain], call this method to signal to the buffer that you
-     * have finished filling in its data fields. The message will be echoed to logcat if
-     * necessary.
+     * have finished filling in its data fields. The message will be echoed to logcat if necessary.
      */
     @Synchronized
     fun commit(message: LogMessage) {
@@ -213,7 +213,8 @@
 
     /** Sends message to echo after determining whether to use Logcat and/or systrace. */
     private fun echoToDesiredEndpoints(message: LogMessage) {
-        val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) ||
+        val includeInLogcat =
+            logcatEchoTracker.isBufferLoggable(name, message.level) ||
                 logcatEchoTracker.isTagLoggable(message.tag, message.level)
         echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
     }
@@ -221,7 +222,12 @@
     /** Converts the entire buffer to a newline-delimited string */
     @Synchronized
     fun dump(pw: PrintWriter, tailLength: Int) {
-        val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) }
+        val iterationStart =
+            if (tailLength <= 0) {
+                0
+            } else {
+                max(0, buffer.size - tailLength)
+            }
 
         for (i in iterationStart until buffer.size) {
             buffer[i].dump(pw)
@@ -229,9 +235,9 @@
     }
 
     /**
-     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called.
-     * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy
-     * values if necessary.
+     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls
+     * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if
+     * necessary.
      */
     @Synchronized
     fun freeze() {
@@ -241,9 +247,7 @@
         }
     }
 
-    /**
-     * Undoes the effects of calling [freeze].
-     */
+    /** Undoes the effects of calling [freeze]. */
     @Synchronized
     fun unfreeze() {
         if (frozen) {
@@ -265,8 +269,11 @@
     }
 
     private fun echoToSystrace(message: LogMessage, strMessage: String) {
-        Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events",
-            "$name - ${message.level.shortString} ${message.tag}: $strMessage")
+        Trace.instantForTrack(
+            Trace.TRACE_TAG_APP,
+            "UI Events",
+            "$name - ${message.level.shortString} ${message.tag}: $strMessage"
+        )
     }
 
     private fun echoToLogcat(message: LogMessage, strMessage: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
similarity index 83%
rename from packages/SystemUI/src/com/android/systemui/log/LogLevel.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
index 53f231c..b036cf0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
@@ -14,17 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
 import android.util.Log
 
-/**
- * Enum version of @Log.Level
- */
-enum class LogLevel(
-    @Log.Level val nativeLevel: Int,
-    val shortString: String
-) {
+/** Enum version of @Log.Level */
+enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) {
     VERBOSE(Log.VERBOSE, "V"),
     DEBUG(Log.DEBUG, "D"),
     INFO(Log.INFO, "I"),
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
index dae2592..9468681 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
 import java.io.PrintWriter
 import java.text.SimpleDateFormat
@@ -29,9 +29,10 @@
  *
  * When a message is logged, the code doing the logging stores data in one or more of the generic
  * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
- * [messagePrinter] function reads the data stored in the generic fields and converts that to a human-
- * readable string. Thus, for every log type there must be a specialized initializer function that
- * stores data specific to that log type and a specialized printer function that prints that data.
+ * [messagePrinter] function reads the data stored in the generic fields and converts that to a
+ * human- readable string. Thus, for every log type there must be a specialized initializer function
+ * that stores data specific to that log type and a specialized printer function that prints that
+ * data.
  *
  * See [LogBuffer.log] for more information.
  */
@@ -55,9 +56,7 @@
     var bool3: Boolean
     var bool4: Boolean
 
-    /**
-     * Function that dumps the [LogMessage] to the provided [writer].
-     */
+    /** Function that dumps the [LogMessage] to the provided [writer]. */
     fun dump(writer: PrintWriter) {
         val formattedTimestamp = DATE_FORMAT.format(timestamp)
         val shortLevel = level.shortString
@@ -68,12 +67,12 @@
 }
 
 /**
- * A function that will be called if and when the message needs to be dumped to
- * logcat or a bug report. It should read the data stored by the initializer and convert it to
- * a human-readable string. The value of `this` will be the LogMessage to be printed.
- * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any
- * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance
- * of the printer for each call, thwarting our attempts at avoiding any sort of allocation.
+ * A function that will be called if and when the message needs to be dumped to logcat or a bug
+ * report. It should read the data stored by the initializer and convert it to a human-readable
+ * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer
+ * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing
+ * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call,
+ * thwarting our attempts at avoiding any sort of allocation.
  */
 typealias MessagePrinter = LogMessage.() -> String
 
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
index 4dd6f65..f2a6a91 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
@@ -14,11 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
-/**
- * Recyclable implementation of [LogMessage].
- */
+/** Recyclable implementation of [LogMessage]. */
 data class LogMessageImpl(
     override var level: LogLevel,
     override var tag: String,
@@ -68,23 +66,24 @@
     companion object Factory {
         fun create(): LogMessageImpl {
             return LogMessageImpl(
-                    LogLevel.DEBUG,
-                    DEFAULT_TAG,
-                    0,
-                    DEFAULT_PRINTER,
-                    null,
-                    null,
-                    null,
-                    null,
-                    0,
-                    0,
-                    0,
-                    0,
-                    0.0,
-                    false,
-                    false,
-                    false,
-                    false)
+                LogLevel.DEBUG,
+                DEFAULT_TAG,
+                0,
+                DEFAULT_PRINTER,
+                null,
+                null,
+                null,
+                null,
+                0,
+                0,
+                0,
+                0,
+                0.0,
+                false,
+                false,
+                false,
+                false
+            )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
index 8cda423..cfe894f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
@@ -14,24 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
-/**
- * Keeps track of which [LogBuffer] messages should also appear in logcat.
- */
+/** Keeps track of which [LogBuffer] messages should also appear in logcat. */
 interface LogcatEchoTracker {
-    /**
-     * Whether [bufferName] should echo messages of [level] or higher to logcat.
-     */
+    /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
     fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean
 
-    /**
-     * Whether [tagName] should echo messages of [level] or higher to logcat.
-     */
+    /** Whether [tagName] should echo messages of [level] or higher to logcat. */
     fun isTagLoggable(tagName: String, level: LogLevel): Boolean
 
-    /**
-     * Whether to log messages in a background thread.
-     */
+    /** Whether to log messages in a background thread. */
     val logInBackgroundThread: Boolean
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
similarity index 73%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
index 40b0cdc..d3fabac 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
 import android.content.ContentResolver
 import android.database.ContentObserver
@@ -36,19 +36,15 @@
  * $ adb shell settings put global systemui/tag/<tag> <level>
  * ```
  */
-class LogcatEchoTrackerDebug private constructor(
-    private val contentResolver: ContentResolver
-) : LogcatEchoTracker {
+class LogcatEchoTrackerDebug private constructor(private val contentResolver: ContentResolver) :
+    LogcatEchoTracker {
     private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf()
     private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf()
     override val logInBackgroundThread = true
 
     companion object Factory {
         @JvmStatic
-        fun create(
-            contentResolver: ContentResolver,
-            mainLooper: Looper
-        ): LogcatEchoTrackerDebug {
+        fun create(contentResolver: ContentResolver, mainLooper: Looper): LogcatEchoTrackerDebug {
             val tracker = LogcatEchoTrackerDebug(contentResolver)
             tracker.attach(mainLooper)
             return tracker
@@ -57,37 +53,35 @@
 
     private fun attach(mainLooper: Looper) {
         contentResolver.registerContentObserver(
-                Settings.Global.getUriFor(BUFFER_PATH),
-                true,
-                object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri?) {
-                        super.onChange(selfChange, uri)
-                        cachedBufferLevels.clear()
-                    }
-                })
+            Settings.Global.getUriFor(BUFFER_PATH),
+            true,
+            object : ContentObserver(Handler(mainLooper)) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    super.onChange(selfChange, uri)
+                    cachedBufferLevels.clear()
+                }
+            }
+        )
 
         contentResolver.registerContentObserver(
-                Settings.Global.getUriFor(TAG_PATH),
-                true,
-                object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri?) {
-                        super.onChange(selfChange, uri)
-                        cachedTagLevels.clear()
-                    }
-                })
+            Settings.Global.getUriFor(TAG_PATH),
+            true,
+            object : ContentObserver(Handler(mainLooper)) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    super.onChange(selfChange, uri)
+                    cachedTagLevels.clear()
+                }
+            }
+        )
     }
 
-    /**
-     * Whether [bufferName] should echo messages of [level] or higher to logcat.
-     */
+    /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
     @Synchronized
     override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
         return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal
     }
 
-    /**
-     * Whether [tagName] should echo messages of [level] or higher to logcat.
-     */
+    /** Whether [tagName] should echo messages of [level] or higher to logcat. */
     @Synchronized
     override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
         return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels)
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
index 1a4ad19..3c8bda4 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
@@ -14,11 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
-/**
- * Production version of [LogcatEchoTracker] that isn't configurable.
- */
+/** Production version of [LogcatEchoTracker] that isn't configurable. */
 class LogcatEchoTrackerProd : LogcatEchoTracker {
     override val logInBackgroundThread = false
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
index 97dc842..68d7890 100644
--- a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.util.collection
+package com.android.systemui.plugins.util
 
 import kotlin.math.max
 
@@ -32,19 +32,16 @@
  * @param factory A function that creates a fresh instance of T. Used by the buffer while it's
  * growing to [maxSize].
  */
-class RingBuffer<T>(
-    private val maxSize: Int,
-    private val factory: () -> T
-) : Iterable<T> {
+class RingBuffer<T>(private val maxSize: Int, private val factory: () -> T) : Iterable<T> {
 
     private val buffer = MutableList<T?>(maxSize) { null }
 
     /**
      * An abstract representation that points to the "end" of the buffer. Increments every time
-     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into
-     * the backing array. Always points to the "next" available slot in the buffer. Before the
-     * buffer has completely filled, the value pointed to will be null. Afterward, it will be the
-     * value at the "beginning" of the buffer.
+     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into the
+     * backing array. Always points to the "next" available slot in the buffer. Before the buffer
+     * has completely filled, the value pointed to will be null. Afterward, it will be the value at
+     * the "beginning" of the buffer.
      *
      * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
      * omega will overflow after a little under three million years of continuous operation.
@@ -60,24 +57,23 @@
 
     /**
      * Advances the buffer's position by one and returns the value that is now present at the "end"
-     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item.
-     * Otherwise, reuses the value that was previously at the "beginning" of the buffer.
+     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. Otherwise,
+     * reuses the value that was previously at the "beginning" of the buffer.
      *
-     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that
-     * was previously stored on it.
+     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that was
+     * previously stored on it.
      */
     fun advance(): T {
         val index = indexOf(omega)
         omega += 1
-        val entry = buffer[index] ?: factory().also {
-            buffer[index] = it
-        }
+        val entry = buffer[index] ?: factory().also { buffer[index] = it }
         return entry
     }
 
     /**
      * Returns the value stored at [index], which can range from 0 (the "start", or oldest element
-     * of the buffer) to [size] - 1 (the "end", or newest element of the buffer).
+     * of the buffer) to [size]
+     * - 1 (the "end", or newest element of the buffer).
      */
     operator fun get(index: Int): T {
         if (index < 0 || index >= size) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
similarity index 74%
rename from packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
rename to packages/SystemUI/plugin/tests/log/LogBufferTest.kt
index 56aff3c..a39b856 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
+++ b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
@@ -2,6 +2,7 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.log.LogBuffer
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -18,8 +19,7 @@
 
     private lateinit var outputWriter: StringWriter
 
-    @Mock
-    private lateinit var logcatEchoTracker: LogcatEchoTracker
+    @Mock private lateinit var logcatEchoTracker: LogcatEchoTracker
 
     @Before
     fun setup() {
@@ -67,15 +67,17 @@
     @Test
     fun dump_writesCauseAndStacktrace() {
         buffer = createBuffer()
-        val exception = createTestException("Exception message",
+        val exception =
+            createTestException(
+                "Exception message",
                 "TestClass",
-                cause = createTestException("The real cause!", "TestClass"))
+                cause = createTestException("The real cause!", "TestClass")
+            )
         buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
 
         val dumpedString = dumpBuffer()
 
-        assertThat(dumpedString)
-                .contains("Caused by: java.lang.RuntimeException: The real cause!")
+        assertThat(dumpedString).contains("Caused by: java.lang.RuntimeException: The real cause!")
         assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
         assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
     }
@@ -85,49 +87,47 @@
         buffer = createBuffer()
         val exception = RuntimeException("Root exception message")
         exception.addSuppressed(
-                createTestException(
-                        "First suppressed exception",
-                        "FirstClass",
-                        createTestException("Cause of suppressed exp", "ThirdClass")
-                ))
-        exception.addSuppressed(
-                createTestException("Second suppressed exception", "SecondClass"))
+            createTestException(
+                "First suppressed exception",
+                "FirstClass",
+                createTestException("Cause of suppressed exp", "ThirdClass")
+            )
+        )
+        exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass"))
         buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
 
         val dumpedStr = dumpBuffer()
 
         // first suppressed exception
         assertThat(dumpedStr)
-                .contains("Suppressed: " +
-                        "java.lang.RuntimeException: First suppressed exception")
+            .contains("Suppressed: " + "java.lang.RuntimeException: First suppressed exception")
         assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)")
         assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)")
 
         assertThat(dumpedStr)
-                .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
+            .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
         assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)")
         assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)")
 
         // second suppressed exception
         assertThat(dumpedStr)
-                .contains("Suppressed: " +
-                        "java.lang.RuntimeException: Second suppressed exception")
+            .contains("Suppressed: " + "java.lang.RuntimeException: Second suppressed exception")
         assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)")
         assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)")
     }
 
     private fun createTestException(
-            message: String,
-            errorClass: String,
-            cause: Throwable? = null,
+        message: String,
+        errorClass: String,
+        cause: Throwable? = null,
     ): Exception {
         val exception = RuntimeException(message, cause)
-        exception.stackTrace = (1..5).map { lineNumber ->
-            StackTraceElement(errorClass,
-                    "TestMethod",
-                    "$errorClass.java",
-                    lineNumber)
-        }.toTypedArray()
+        exception.stackTrace =
+            (1..5)
+                .map { lineNumber ->
+                    StackTraceElement(errorClass, "TestMethod", "$errorClass.java", lineNumber)
+                }
+                .toTypedArray()
         return exception
     }
 
diff --git a/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
new file mode 100644
index 0000000..de0e526
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.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" >
+    <size android:height="@dimen/bouncer_user_switcher_popup_items_divider_height"/>
+    <solid android:color="@color/user_switcher_fullscreen_bg"/>
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 46f6ab2..0a55cf7 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -119,6 +119,7 @@
     <dimen name="bouncer_user_switcher_width">248dp</dimen>
     <dimen name="bouncer_user_switcher_popup_header_height">12dp</dimen>
     <dimen name="bouncer_user_switcher_popup_divider_height">4dp</dimen>
+    <dimen name="bouncer_user_switcher_popup_items_divider_height">2dp</dimen>
     <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen>
     <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen>
     <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen>
diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
index bc8e540..3bcc37a 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
@@ -16,45 +16,47 @@
 
 <com.android.systemui.biometrics.AuthCredentialPasswordView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPinPasswordStyle">
 
     <RelativeLayout
         android:id="@+id/auth_credential_header"
-        style="@style/AuthCredentialHeaderStyle"
+        style="?headerStyle"
         android:layout_width="wrap_content"
         android:layout_height="match_parent">
 
         <ImageView
             android:id="@+id/icon"
-            style="@style/TextAppearance.AuthNonBioCredential.Icon"
+            style="?headerIconStyle"
             android:layout_alignParentLeft="true"
             android:layout_alignParentTop="true"
             android:contentDescription="@null"/>
 
         <TextView
             android:id="@+id/title"
-            style="@style/TextAppearance.AuthNonBioCredential.Title"
+            style="?titleTextAppearance"
             android:layout_below="@id/icon"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/subtitle"
-            style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
+            style="?subTitleTextAppearance"
             android:layout_below="@id/title"
             android:layout_alignParentLeft="true"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/description"
-            style="@style/TextAppearance.AuthNonBioCredential.Description"
+            style="?descriptionTextAppearance"
             android:layout_below="@id/subtitle"
             android:layout_alignParentLeft="true"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
     </RelativeLayout>
@@ -67,7 +69,7 @@
 
         <ImeAwareEditText
             android:id="@+id/lockPassword"
-            style="@style/TextAppearance.AuthCredential.PasswordEntry"
+            style="?passwordTextAppearance"
             android:layout_width="208dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
@@ -77,7 +79,7 @@
 
         <TextView
             android:id="@+id/error"
-            style="@style/TextAppearance.AuthNonBioCredential.Error"
+            style="?errorTextAppearance"
             android:layout_gravity="center"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content" />
diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
index 19a85fe..a3dd334 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
@@ -16,91 +16,71 @@
 
 <com.android.systemui.biometrics.AuthCredentialPatternView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPatternStyle">
 
-    <LinearLayout
+    <RelativeLayout
+        android:id="@+id/auth_credential_header"
+        style="?headerStyle"
         android:layout_width="0dp"
         android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:gravity="center"
-        android:orientation="vertical">
-
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
+        android:layout_weight="1">
 
         <ImageView
             android:id="@+id/icon"
+            style="?headerIconStyle"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"
+            android:contentDescription="@null"/>
+
+        <TextView
+            android:id="@+id/title"
+            style="?titleTextAppearance"
+            android:layout_below="@id/icon"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
 
         <TextView
-            android:id="@+id/title"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Title"/>
-
-        <TextView
             android:id="@+id/subtitle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Subtitle"/>
+            style="?subTitleTextAppearance"
+            android:layout_below="@id/title"
+            android:layout_alignParentLeft="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/description"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Description"/>
+            style="?descriptionTextAppearance"
+            android:layout_below="@id/subtitle"
+            android:layout_alignParentLeft="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
+    </RelativeLayout>
+
+    <FrameLayout
+        android:layout_weight="1"
+        style="?containerStyle"
+        android:layout_width="0dp"
+        android:layout_height="match_parent">
+
+        <com.android.internal.widget.LockPatternView
+            android:id="@+id/lockPattern"
+            android:layout_gravity="center"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
 
         <TextView
             android:id="@+id/error"
+            style="?errorTextAppearance"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Error"/>
+            android:layout_gravity="center_horizontal|bottom"/>
 
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
-
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:orientation="vertical"
-        android:gravity="center"
-        android:paddingLeft="0dp"
-        android:paddingRight="0dp"
-        android:paddingTop="0dp"
-        android:paddingBottom="16dp"
-        android:clipToPadding="false">
-
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="0dp"
-            android:layout_weight="1"
-            style="@style/LockPatternContainerStyle">
-
-            <com.android.internal.widget.LockPatternView
-                android:id="@+id/lockPattern"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:layout_gravity="center"
-                style="@style/LockPatternStyleBiometricPrompt"/>
-
-        </FrameLayout>
-
-    </LinearLayout>
+    </FrameLayout>
 
 </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml
index 75a80bc..774b335f 100644
--- a/packages/SystemUI/res/layout/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml
@@ -16,43 +16,45 @@
 
 <com.android.systemui.biometrics.AuthCredentialPasswordView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:elevation="@dimen/biometric_dialog_elevation"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:theme="?app:attr/lockPinPasswordStyle">
 
     <RelativeLayout
         android:id="@+id/auth_credential_header"
-        style="@style/AuthCredentialHeaderStyle"
+        style="?headerStyle"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
         <ImageView
             android:id="@+id/icon"
-            style="@style/TextAppearance.AuthNonBioCredential.Icon"
+            style="?headerIconStyle"
             android:layout_alignParentLeft="true"
             android:layout_alignParentTop="true"
             android:contentDescription="@null"/>
 
         <TextView
             android:id="@+id/title"
-            style="@style/TextAppearance.AuthNonBioCredential.Title"
+            style="?titleTextAppearance"
             android:layout_below="@id/icon"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
 
         <TextView
             android:id="@+id/subtitle"
-            style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
+            style="?subTitleTextAppearance"
             android:layout_below="@id/title"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
 
         <TextView
             android:id="@+id/description"
-            style="@style/TextAppearance.AuthNonBioCredential.Description"
+            style="?descriptionTextAppearance"
             android:layout_below="@id/subtitle"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
     </RelativeLayout>
 
@@ -64,7 +66,7 @@
 
         <ImeAwareEditText
             android:id="@+id/lockPassword"
-            style="@style/TextAppearance.AuthCredential.PasswordEntry"
+            style="?passwordTextAppearance"
             android:layout_width="208dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center_horizontal"
@@ -74,7 +76,7 @@
 
         <TextView
             android:id="@+id/error"
-            style="@style/TextAppearance.AuthNonBioCredential.Error"
+            style="?errorTextAppearance"
             android:layout_gravity="center_horizontal"
             android:layout_width="match_parent"
             android:layout_height="wrap_content" />
diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
index dada981..4af9970 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -16,87 +16,66 @@
 
 <com.android.systemui.biometrics.AuthCredentialPatternView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
-    android:gravity="center_horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPatternStyle">
 
     <RelativeLayout
+        android:id="@+id/auth_credential_header"
+        style="?headerStyle"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
+        android:layout_height="wrap_content">
 
-        <LinearLayout
-            android:id="@+id/auth_credential_header"
-            style="@style/AuthCredentialHeaderStyle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content">
+        <ImageView
+            android:id="@+id/icon"
+            style="?headerIconStyle"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"
+            android:contentDescription="@null"/>
 
-            <ImageView
-                android:id="@+id/icon"
-                android:layout_width="48dp"
-                android:layout_height="48dp"
-                android:contentDescription="@null" />
+        <TextView
+            android:id="@+id/title"
+            style="?titleTextAppearance"
+            android:layout_below="@id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-            <TextView
-                android:id="@+id/title"
-                style="@style/TextAppearance.AuthNonBioCredential.Title"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
+        <TextView
+            android:id="@+id/subtitle"
+            style="?subTitleTextAppearance"
+            android:layout_below="@id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-            <TextView
-                android:id="@+id/subtitle"
-                style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
-
-            <TextView
-                android:id="@+id/description"
-                style="@style/TextAppearance.AuthNonBioCredential.Description"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
-        </LinearLayout>
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_below="@id/auth_credential_header"
-            android:gravity="center"
-            android:orientation="vertical"
-            android:paddingBottom="16dp"
-            android:paddingTop="60dp">
-
-            <FrameLayout
-                style="@style/LockPatternContainerStyle"
-                android:layout_width="wrap_content"
-                android:layout_height="0dp"
-                android:layout_weight="1">
-
-                <com.android.internal.widget.LockPatternView
-                    android:id="@+id/lockPattern"
-                    style="@style/LockPatternStyle"
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent"
-                    android:layout_gravity="center" />
-
-            </FrameLayout>
-
-        </LinearLayout>
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentBottom="true">
-
-            <TextView
-                android:id="@+id/error"
-                style="@style/TextAppearance.AuthNonBioCredential.Error"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
-
-        </LinearLayout>
-
+        <TextView
+            android:id="@+id/description"
+            style="?descriptionTextAppearance"
+            android:layout_below="@id/subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
     </RelativeLayout>
 
+    <FrameLayout
+        android:id="@+id/auth_credential_container"
+        style="?containerStyle"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.android.internal.widget.LockPatternView
+            android:id="@+id/lockPattern"
+            android:layout_gravity="center"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+        <TextView
+            android:id="@+id/error"
+            style="?errorTextAppearance"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal|bottom"/>
+    </FrameLayout>
+
 </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml
index 4da7711..bc97e51 100644
--- a/packages/SystemUI/res/layout/chipbar.xml
+++ b/packages/SystemUI/res/layout/chipbar.xml
@@ -19,12 +19,12 @@
 <com.android.systemui.temporarydisplay.chipbar.ChipbarRootView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/media_ttt_sender_chip"
+    android:id="@+id/chipbar_root_view"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content">
 
     <LinearLayout
-        android:id="@+id/media_ttt_sender_chip_inner"
+        android:id="@+id/chipbar_inner"
         android:orientation="horizontal"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -39,7 +39,7 @@
         >
 
         <com.android.internal.widget.CachingIconView
-            android:id="@+id/app_icon"
+            android:id="@+id/start_icon"
             android:layout_width="@dimen/media_ttt_app_icon_size"
             android:layout_height="@dimen/media_ttt_app_icon_size"
             android:layout_marginEnd="12dp"
@@ -69,7 +69,7 @@
             />
 
         <ImageView
-            android:id="@+id/failure_icon"
+            android:id="@+id/error"
             android:layout_width="@dimen/media_ttt_status_icon_size"
             android:layout_height="@dimen/media_ttt_status_icon_size"
             android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
@@ -78,11 +78,11 @@
             android:alpha="0.0"
             />
 
+        <!-- TODO(b/245610654): Re-name all the media-specific dimens to chipbar dimens instead. -->
         <TextView
-            android:id="@+id/undo"
+            android:id="@+id/end_button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="@string/media_transfer_undo"
             android:textColor="?androidprv:attr/textColorOnAccent"
             android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
             android:textSize="@dimen/media_ttt_text_size"
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index 5dc34b9..a565988 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -73,8 +73,8 @@
         android:singleLine="true"
         android:textDirection="locale"
         android:textAppearance="@style/TextAppearance.QS.Status"
-        android:transformPivotX="0sp"
-        android:transformPivotY="20sp"
+        android:transformPivotX="0dp"
+        android:transformPivotY="24dp"
         android:scaleX="1"
         android:scaleY="1"
     />
diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml
index ac9a947..aefd998 100644
--- a/packages/SystemUI/res/values-land/styles.xml
+++ b/packages/SystemUI/res/values-land/styles.xml
@@ -24,7 +24,36 @@
         <item name="android:paddingEnd">24dp</item>
         <item name="android:paddingTop">48dp</item>
         <item name="android:paddingBottom">10dp</item>
-        <item name="android:gravity">top|center_horizontal</item>
+        <item name="android:gravity">top|left</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">320dp</item>
+        <item name="android:maxWidth">320dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">60dp</item>
+        <item name="android:paddingVertical">20dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">36dp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">18sp</item>
     </style>
 
 </resources>
diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml
new file mode 100644
index 0000000..8148d3d
--- /dev/null
+++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml
@@ -0,0 +1,47 @@
+<?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:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">120dp</item>
+        <item name="android:paddingVertical">40dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+</resources>
diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml
new file mode 100644
index 0000000..771de08
--- /dev/null
+++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml
@@ -0,0 +1,44 @@
+<?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:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialHeaderStyle">
+        <item name="android:paddingStart">120dp</item>
+        <item name="android:paddingEnd">120dp</item>
+        <item name="android:paddingTop">80dp</item>
+        <item name="android:paddingBottom">10dp</item>
+        <item name="android:layout_gravity">top</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">180dp</item>
+        <item name="android:paddingVertical">80dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml
new file mode 100644
index 0000000..f9ed67d
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml
@@ -0,0 +1,48 @@
+<?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:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">120dp</item>
+        <item name="android:paddingVertical">40dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml
new file mode 100644
index 0000000..78d299c
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml
@@ -0,0 +1,44 @@
+<?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:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialHeaderStyle">
+        <item name="android:paddingStart">120dp</item>
+        <item name="android:paddingEnd">120dp</item>
+        <item name="android:paddingTop">80dp</item>
+        <item name="android:paddingBottom">10dp</item>
+        <item name="android:layout_gravity">top</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">240dp</item>
+        <item name="android:paddingVertical">120dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 9a71995..df0659d 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -191,5 +191,18 @@
     <declare-styleable name="DelayableMarqueeTextView">
         <attr name="marqueeDelay" format="integer" />
     </declare-styleable>
+
+    <declare-styleable name="AuthCredentialView">
+        <attr name="lockPatternStyle" format="reference" />
+        <attr name="lockPinPasswordStyle" format="reference" />
+        <attr name="containerStyle" format="reference" />
+        <attr name="headerStyle" format="reference" />
+        <attr name="headerIconStyle" format="reference" />
+        <attr name="titleTextAppearance" format="reference" />
+        <attr name="subTitleTextAppearance" format="reference" />
+        <attr name="descriptionTextAppearance" format="reference" />
+        <attr name="passwordTextAppearance" format="reference" />
+        <attr name="errorTextAppearance" format="reference"/>
+    </declare-styleable>
 </resources>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 01c9ac1..66f0e75 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -519,7 +519,7 @@
     <dimen name="qs_tile_margin_horizontal">8dp</dimen>
     <dimen name="qs_tile_margin_vertical">@dimen/qs_tile_margin_horizontal</dimen>
     <dimen name="qs_tile_margin_top_bottom">4dp</dimen>
-    <dimen name="qs_brightness_margin_top">8dp</dimen>
+    <dimen name="qs_brightness_margin_top">12dp</dimen>
     <dimen name="qs_brightness_margin_bottom">16dp</dimen>
     <dimen name="qqs_layout_margin_top">16dp</dimen>
     <dimen name="qqs_layout_padding_bottom">24dp</dimen>
@@ -572,6 +572,7 @@
     <dimen name="qs_header_row_min_height">48dp</dimen>
 
     <dimen name="qs_header_non_clickable_element_height">24dp</dimen>
+    <dimen name="new_qs_header_non_clickable_element_height">20dp</dimen>
 
     <dimen name="qs_footer_padding">20dp</dimen>
     <dimen name="qs_security_footer_height">88dp</dimen>
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index 3164ed1..e30d441 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -28,4 +28,11 @@
 
     <!-- The time it takes for the over scroll release animation to complete, in milli seconds.  -->
     <integer name="lockscreen_shade_over_scroll_release_duration">0</integer>
+
+    <!-- Values for transition of QS Headers -->
+    <integer name="fade_out_complete_frame">14</integer>
+    <integer name="fade_in_start_frame">58</integer>
+    <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at
+         fade_out_complete_frame -->
+    <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 637ac19..d4d8843 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -641,7 +641,7 @@
     <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_color_correction_label">Color correction</string>
     <!-- QuickSettings: Control panel: Label for button that navigates to user settings. [CHAR LIMIT=NONE] -->
-    <string name="quick_settings_more_user_settings">User settings</string>
+    <string name="quick_settings_more_user_settings">Manage users</string>
     <!-- QuickSettings: Control panel: Label for button that dismisses control panel. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_done">Done</string>
     <!-- QuickSettings: Control panel: Label for button that dismisses user switcher control panel. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index a734fa7..e76887b 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -128,11 +128,10 @@
     <!-- This is hard coded to be sans-serif-condensed to match the icons -->
 
     <style name="TextAppearance.QS.Status">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textSize">14sp</item>
         <item name="android:letterSpacing">0.01</item>
-        <item name="android:lineHeight">20sp</item>
     </style>
 
     <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status">
@@ -143,12 +142,10 @@
     <style name="TextAppearance.QS.Status.Carriers" />
 
     <style name="TextAppearance.QS.Status.Carriers.NoCarrierText">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
     <style name="TextAppearance.QS.Status.Build">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
@@ -198,15 +195,11 @@
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
-    <style name="TextAppearance.AuthNonBioCredential.Icon">
-        <item name="android:layout_width">@dimen/biometric_auth_icon_size</item>
-        <item name="android:layout_height">@dimen/biometric_auth_icon_size</item>
-    </style>
-
     <style name="TextAppearance.AuthNonBioCredential.Title">
         <item name="android:fontFamily">google-sans</item>
-        <item name="android:layout_marginTop">20dp</item>
-        <item name="android:textSize">36sp</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36dp</item>
+        <item name="android:focusable">true</item>
     </style>
 
     <style name="TextAppearance.AuthNonBioCredential.Subtitle">
@@ -218,12 +211,10 @@
     <style name="TextAppearance.AuthNonBioCredential.Description">
         <item name="android:fontFamily">google-sans</item>
         <item name="android:layout_marginTop">20dp</item>
-        <item name="android:textSize">16sp</item>
+        <item name="android:textSize">18sp</item>
     </style>
 
     <style name="TextAppearance.AuthNonBioCredential.Error">
-        <item name="android:paddingTop">6dp</item>
-        <item name="android:paddingBottom">18dp</item>
         <item name="android:paddingHorizontal">24dp</item>
         <item name="android:textSize">14sp</item>
         <item name="android:textColor">?android:attr/colorError</item>
@@ -242,12 +233,33 @@
     <style name="AuthCredentialHeaderStyle">
         <item name="android:paddingStart">48dp</item>
         <item name="android:paddingEnd">48dp</item>
-        <item name="android:paddingTop">28dp</item>
-        <item name="android:paddingBottom">20dp</item>
-        <item name="android:orientation">vertical</item>
+        <item name="android:paddingTop">48dp</item>
+        <item name="android:paddingBottom">10dp</item>
         <item name="android:layout_gravity">top</item>
     </style>
 
+    <style name="AuthCredentialIconStyle">
+        <item name="android:layout_width">@dimen/biometric_auth_icon_size</item>
+        <item name="android:layout_height">@dimen/biometric_auth_icon_size</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:padding">20dp</item>
+    </style>
+
+    <style name="AuthCredentialPinPasswordContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">48dp</item>
+        <item name="android:maxWidth">600dp</item>
+        <item name="android:minHeight">48dp</item>
+        <item name="android:minWidth">200dp</item>
+    </style>
+
     <style name="DeviceManagementDialogTitle">
         <item name="android:gravity">center</item>
         <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item>
@@ -285,7 +297,9 @@
         <item name="wallpaperTextColorSecondary">@*android:color/secondary_text_material_dark</item>
         <item name="wallpaperTextColorAccent">@color/material_dynamic_primary90</item>
         <item name="android:colorError">@*android:color/error_color_material_dark</item>
-        <item name="*android:lockPatternStyle">@style/LockPatternStyle</item>
+        <item name="*android:lockPatternStyle">@style/LockPatternViewStyle</item>
+        <item name="lockPatternStyle">@style/LockPatternContainerStyle</item>
+        <item name="lockPinPasswordStyle">@style/LockPinPasswordContainerStyle</item>
         <item name="passwordStyle">@style/PasswordTheme</item>
         <item name="numPadKeyStyle">@style/NumPadKey</item>
         <item name="backgroundProtectedStyle">@style/BackgroundProtectedStyle</item>
@@ -311,27 +325,33 @@
         <item name="android:textColor">?attr/wallpaperTextColor</item>
     </style>
 
-    <style name="LockPatternContainerStyle">
-        <item name="android:maxHeight">400dp</item>
-        <item name="android:maxWidth">420dp</item>
-        <item name="android:minHeight">0dp</item>
-        <item name="android:minWidth">0dp</item>
-        <item name="android:paddingHorizontal">60dp</item>
-        <item name="android:paddingBottom">40dp</item>
+    <style name="AuthCredentialStyle">
+        <item name="*android:regularColor">?android:attr/colorForeground</item>
+        <item name="*android:successColor">?android:attr/colorForeground</item>
+        <item name="*android:errorColor">?android:attr/colorError</item>
+        <item name="*android:dotColor">?android:attr/textColorSecondary</item>
+        <item name="headerStyle">@style/AuthCredentialHeaderStyle</item>
+        <item name="headerIconStyle">@style/AuthCredentialIconStyle</item>
+        <item name="titleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Title</item>
+        <item name="subTitleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Subtitle</item>
+        <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item>
+        <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item>
+        <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item>
     </style>
 
-    <style name="LockPatternStyle">
+    <style name="LockPatternViewStyle" >
         <item name="*android:regularColor">?android:attr/colorAccent</item>
         <item name="*android:successColor">?android:attr/textColorPrimary</item>
         <item name="*android:errorColor">?android:attr/colorError</item>
         <item name="*android:dotColor">?android:attr/textColorSecondary</item>
     </style>
 
-    <style name="LockPatternStyleBiometricPrompt">
-        <item name="*android:regularColor">?android:attr/colorForeground</item>
-        <item name="*android:successColor">?android:attr/colorForeground</item>
-        <item name="*android:errorColor">?android:attr/colorError</item>
-        <item name="*android:dotColor">?android:attr/textColorSecondary</item>
+    <style name="LockPatternContainerStyle" parent="@style/AuthCredentialStyle">
+        <item name="containerStyle">@style/AuthCredentialPatternContainerStyle</item>
+    </style>
+
+    <style name="LockPinPasswordContainerStyle" parent="@style/AuthCredentialStyle">
+        <item name="containerStyle">@style/AuthCredentialPinPasswordContainerStyle</item>
     </style>
 
     <style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault">
diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
index f3866c0..de855e2 100644
--- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml
+++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
@@ -27,67 +27,60 @@
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="49"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:sizePercent="0"
                 app:curveFit="linear"
                 app:motionTarget="@id/date" />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
+                app:percentY="0.5"
                 app:sizePercent="1"
-                app:framePosition="51"
+                app:framePosition="50"
                 app:curveFit="linear"
                 app:motionTarget="@id/date" />
             <KeyAttribute
                 app:motionTarget="@id/date"
-                app:framePosition="30"
+                app:framePosition="14"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/date"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
-                app:keyPositionType="pathRelative"
+                app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="0"
-                app:curveFit="linear"
-                app:motionTarget="@id/statusIcons" />
-            <KeyPosition
-                app:keyPositionType="pathRelative"
-                app:percentX="0"
-                app:percentY="0"
-                app:framePosition="50"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:sizePercent="0"
                 app:curveFit="linear"
                 app:motionTarget="@id/statusIcons" />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:sizePercent="1"
                 app:curveFit="linear"
                 app:motionTarget="@id/statusIcons" />
             <KeyAttribute
                 app:motionTarget="@id/statusIcons"
-                app:framePosition="30"
+                app:framePosition="@integer/fade_out_complete_frame"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/statusIcons"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="50"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
@@ -95,27 +88,27 @@
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
                 app:motionTarget="@id/batteryRemainingIcon" />
             <KeyAttribute
                 app:motionTarget="@id/batteryRemainingIcon"
-                app:framePosition="30"
+                app:framePosition="@integer/fade_out_complete_frame"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/batteryRemainingIcon"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
                 app:motionTarget="@id/carrier_group"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
@@ -126,7 +119,7 @@
                 android:alpha="0" />
             <KeyAttribute
                 app:motionTarget="@id/carrier_group"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0" />
         </KeyFrameSet>
     </Transition>
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index a82684d03..88b4f43 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -43,7 +43,8 @@
         android:id="@+id/date">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            android:layout_marginStart="8dp"
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/clock"
             app:layout_constraintEnd_toStartOf="@id/barrier"
@@ -57,8 +58,8 @@
         android:id="@+id/statusIcons">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/date"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
             app:layout_constraintTop_toTopOf="parent"
@@ -71,9 +72,9 @@
         android:id="@+id/batteryRemainingIcon">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constrainedWidth="true"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="@id/end_guide"
             app:layout_constraintTop_toTopOf="parent"
diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml
index f39e6bd..d8a4e77 100644
--- a/packages/SystemUI/res/xml/qs_header_new.xml
+++ b/packages/SystemUI/res/xml/qs_header_new.xml
@@ -40,13 +40,13 @@
             android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/privacy_container"
-            app:layout_constraintBottom_toTopOf="@id/date"
+            app:layout_constraintBottom_toBottomOf="@id/carrier_group"
             app:layout_constraintEnd_toStartOf="@id/carrier_group"
             app:layout_constraintHorizontal_bias="0"
         />
         <Transform
-            android:scaleX="2.4"
-            android:scaleY="2.4"
+            android:scaleX="2.57"
+            android:scaleY="2.57"
             />
     </Constraint>
 
@@ -54,11 +54,11 @@
         android:id="@+id/date">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toStartOf="@id/space"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/clock"
+            app:layout_constraintTop_toBottomOf="@id/carrier_group"
             app:layout_constraintHorizontal_bias="0"
             app:layout_constraintHorizontal_chainStyle="spread_inside"
         />
@@ -87,7 +87,7 @@
         android:id="@+id/statusIcons">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/space"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
@@ -101,8 +101,8 @@
         android:id="@+id/batteryRemainingIcon">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="@id/date"
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
index 2e391c7..49cc483 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.graphics.Color
 import android.view.View
+import android.view.Window
 import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
@@ -51,13 +52,14 @@
 
     /**
      * Compare the content of the [view] with the golden image identified by [goldenIdentifier] in
-     * the context of [emulationSpec].
+     * the context of [emulationSpec]. Window must be specified to capture views that render
+     * hardware buffers.
      */
-    fun screenshotTest(goldenIdentifier: String, view: View) {
+    fun screenshotTest(goldenIdentifier: String, view: View, window: Window? = null) {
         view.removeElevationRecursively()
 
         ScreenshotRuleAsserter.Builder(screenshotRule)
-            .setScreenshotProvider { view.toBitmap() }
+            .setScreenshotProvider { view.toBitmap(window) }
             .withMatcher(matcher)
             .build()
             .assertGoldenImage(goldenIdentifier)
@@ -94,6 +96,6 @@
             activity.currentFocus?.clearFocus()
         }
 
-        screenshotTest(goldenIdentifier, rootView)
+        screenshotTest(goldenIdentifier, rootView, activity.window)
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 134f3bc..1cf7c50 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -190,8 +190,13 @@
 
     override fun onDraw(canvas: Canvas) {
         lastDraw = getTimestamp()
-        // intentionally doesn't call super.onDraw here or else the text will be rendered twice
-        textAnimator?.draw(canvas)
+        // Use textAnimator to render text if animation is enabled.
+        // Otherwise default to using standard draw functions.
+        if (isAnimationEnabled) {
+            textAnimator?.draw(canvas)
+        } else {
+            super.onDraw(canvas)
+        }
     }
 
     override fun invalidate() {
@@ -363,6 +368,9 @@
                 onAnimationEnd = onAnimationEnd
             )
             textAnimator?.glyphFilter = glyphFilter
+            if (color != null && !isAnimationEnabled) {
+                setTextColor(color)
+            }
         } else {
             // when the text animator is set, update its start values
             onTextAnimatorInitialized = Runnable {
@@ -377,6 +385,9 @@
                     onAnimationEnd = onAnimationEnd
                 )
                 textAnimator?.glyphFilter = glyphFilter
+                if (color != null && !isAnimationEnabled) {
+                    setTextColor(color)
+                }
             }
         }
     }
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 f03fee4..cd27263 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
@@ -21,7 +21,7 @@
 import android.os.UserHandle
 import android.provider.Settings
 import android.util.Log
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.internal.annotations.Keep
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
@@ -30,7 +30,6 @@
 import com.android.systemui.plugins.PluginListener
 import com.android.systemui.shared.plugins.PluginManager
 import com.google.gson.Gson
-import javax.inject.Inject
 
 private val TAG = ClockRegistry::class.simpleName
 private const val DEBUG = true
@@ -42,13 +41,6 @@
     val handler: Handler,
     defaultClockProvider: ClockProvider
 ) {
-    @Inject constructor(
-        context: Context,
-        pluginManager: PluginManager,
-        @Main handler: Handler,
-        defaultClockProvider: DefaultClockProvider
-    ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { }
-
     // Usually this would be a typealias, but a SAM provides better java interop
     fun interface ClockChangeListener {
         fun onClockChanged()
@@ -201,6 +193,7 @@
         val provider: ClockProvider
     )
 
+    @Keep
     private data class ClockSetting(
         val clockId: ClockId,
         val _applied_timestamp: Long?
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
index 72f8b7b..40c8774 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -1,13 +1,16 @@
 package com.android.systemui.shared.recents.utilities;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.view.Surface;
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.wm.shell.util.SplitBounds;
 
 /**
  * Utility class to position the thumbnail in the TaskView
@@ -16,10 +19,26 @@
 
     public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f;
 
+    /**
+     * Specifies that a stage is positioned at the top half of the screen if
+     * in portrait mode or at the left half of the screen if in landscape mode.
+     * TODO(b/254378592): Remove after consolidation
+     */
+    public static final int STAGE_POSITION_TOP_OR_LEFT = 0;
+
+    /**
+     * Specifies that a stage is positioned at the bottom half of the screen if
+     * in portrait mode or at the right half of the screen if in landscape mode.
+     * TODO(b/254378592): Remove after consolidation
+     */
+    public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1;
+
     // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1.
     private final RectF mClippedInsets = new RectF();
     private final Matrix mMatrix = new Matrix();
     private boolean mIsOrientationChanged;
+    private SplitBounds mSplitBounds;
+    private int mDesiredStagePosition;
 
     public Matrix getMatrix() {
         return mMatrix;
@@ -33,6 +52,11 @@
         return mIsOrientationChanged;
     }
 
+    public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) {
+        mSplitBounds = splitBounds;
+        mDesiredStagePosition = desiredStagePosition;
+    }
+
     /**
      * Updates the matrix based on the provided parameters
      */
@@ -42,10 +66,19 @@
         boolean isRotated = false;
         boolean isOrientationDifferent;
 
+        float fullscreenTaskWidth = screenWidthPx;
+        if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) {
+            // For landscape, scale the width
+            float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
+                    ? mSplitBounds.leftTaskPercent
+                    : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent));
+            // Scale landscape width to that of actual screen
+            fullscreenTaskWidth = screenWidthPx * taskPercent;
+        }
         int thumbnailRotation = thumbnailData.rotation;
         int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
         RectF thumbnailClipHint = new RectF();
-        float canvasScreenRatio = canvasWidth / (float) screenWidthPx;
+        float canvasScreenRatio = canvasWidth / fullscreenTaskWidth;
         float scaledTaskbarSize = taskbarSize * canvasScreenRatio;
         thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0;
 
@@ -180,7 +213,7 @@
      * portrait or vice versa, {@code false} otherwise
      */
     private boolean isOrientationChange(int deltaRotation) {
-        return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+        return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270;
     }
 
     private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) {
@@ -189,13 +222,13 @@
 
         mMatrix.setRotate(90 * deltaRotate);
         switch (deltaRotate) { /* Counter-clockwise */
-            case Surface.ROTATION_90:
+            case ROTATION_90:
                 translateX = thumbnailPosition.height();
                 break;
-            case Surface.ROTATION_270:
+            case ROTATION_270:
                 translateY = thumbnailPosition.width();
                 break;
-            case Surface.ROTATION_180:
+            case ROTATION_180:
                 translateX = thumbnailPosition.width();
                 translateY = thumbnailPosition.height();
                 break;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
index dd2e55d..cd4b999 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.shared.regionsampling
 
+import android.graphics.Color
 import android.graphics.Rect
 import android.view.View
 import androidx.annotation.VisibleForTesting
@@ -33,18 +34,19 @@
         regionSamplingEnabled: Boolean,
         updateFun: UpdateColorCallback
 ) {
-    private var isDark = RegionDarkness.DEFAULT
+    private var regionDarkness = RegionDarkness.DEFAULT
     private var samplingBounds = Rect()
     private val tmpScreenLocation = IntArray(2)
     @VisibleForTesting var regionSampler: RegionSamplingHelper? = null
-
+    private var lightForegroundColor = Color.WHITE
+    private var darkForegroundColor = Color.BLACK
     /**
      * Interface for method to be passed into RegionSamplingHelper
      */
     @FunctionalInterface
     interface UpdateColorCallback {
         /**
-         * Method to update the text colors after clock darkness changed.
+         * Method to update the foreground colors after clock darkness changed.
          */
         fun updateColors()
     }
@@ -59,6 +61,30 @@
         return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
     }
 
+    /**
+     * Sets the colors to be used for Dark and Light Foreground.
+     *
+     * @param lightColor The color used for Light Foreground.
+     * @param darkColor The color used for Dark Foreground.
+     */
+    fun setForegroundColors(lightColor: Int, darkColor: Int) {
+        lightForegroundColor = lightColor
+        darkForegroundColor = darkColor
+    }
+
+    /**
+     * Determines which foreground color to use based on region darkness.
+     *
+     * @return the determined foreground color
+     */
+    fun currentForegroundColor(): Int{
+        return if (regionDarkness.isDark) {
+            lightForegroundColor
+        } else {
+            darkForegroundColor
+        }
+    }
+
     private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
         return if (isRegionDark) {
             RegionDarkness.DARK
@@ -68,7 +94,7 @@
     }
 
     fun currentRegionDarkness(): RegionDarkness {
-        return isDark
+        return regionDarkness
     }
 
     /**
@@ -97,7 +123,7 @@
             regionSampler = createRegionSamplingHelper(sampledView,
                     object : SamplingCallback {
                         override fun onRegionDarknessChanged(isRegionDark: Boolean) {
-                            isDark = convertToClockDarkness(isRegionDark)
+                            regionDarkness = convertToClockDarkness(isRegionDark)
                             updateFun.updateColors()
                         }
                         /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
index 5d6598d..8a25096 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
@@ -51,6 +51,8 @@
             InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER;
     public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION =
             InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
+    public static final int CUJ_RECENTS_SCROLLING =
+            InteractionJankMonitor.CUJ_RECENTS_SCROLLING;
 
     @IntDef({
             CUJ_APP_LAUNCH_FROM_RECENTS,
@@ -59,7 +61,8 @@
             CUJ_APP_CLOSE_TO_PIP,
             CUJ_QUICK_SWITCH,
             CUJ_APP_LAUNCH_FROM_WIDGET,
-            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION
+            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
+            CUJ_RECENTS_SCROLLING
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
deleted file mode 100644
index 30c062b..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2018 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.system;
-
-import android.graphics.HardwareRenderer;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.Message;
-import android.os.Trace;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-import android.view.View;
-import android.view.ViewRootImpl;
-
-import java.util.function.Consumer;
-
-/**
- * Helper class to apply surface transactions in sync with RenderThread.
- *
- * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't 
- *       currently reference that class from the shared lib as it is hidden.
- */
-public class SyncRtSurfaceTransactionApplierCompat {
-
-    public static final int FLAG_ALL = 0xffffffff;
-    public static final int FLAG_ALPHA = 1;
-    public static final int FLAG_MATRIX = 1 << 1;
-    public static final int FLAG_WINDOW_CROP = 1 << 2;
-    public static final int FLAG_LAYER = 1 << 3;
-    public static final int FLAG_CORNER_RADIUS = 1 << 4;
-    public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
-    public static final int FLAG_VISIBILITY = 1 << 6;
-    public static final int FLAG_RELATIVE_LAYER = 1 << 7;
-    public static final int FLAG_SHADOW_RADIUS = 1 << 8;
-
-    private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
-
-    private final SurfaceControl mBarrierSurfaceControl;
-    private final ViewRootImpl mTargetViewRootImpl;
-    private final Handler mApplyHandler;
-
-    private int mSequenceNumber = 0;
-    private int mPendingSequenceNumber = 0;
-    private Runnable mAfterApplyCallback;
-
-    /**
-     * @param targetView The view in the surface that acts as synchronization anchor.
-     */
-    public SyncRtSurfaceTransactionApplierCompat(View targetView) {
-        mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
-        mBarrierSurfaceControl = mTargetViewRootImpl != null
-            ? mTargetViewRootImpl.getSurfaceControl() : null;
-
-        mApplyHandler = new Handler(new Callback() {
-            @Override
-            public boolean handleMessage(Message msg) {
-                if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
-                    onApplyMessage(msg.arg1);
-                    return true;
-                }
-                return false;
-            }
-        });
-    }
-
-    private void onApplyMessage(int seqNo) {
-        mSequenceNumber = seqNo;
-        if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) {
-            Runnable r = mAfterApplyCallback;
-            mAfterApplyCallback = null;
-            r.run();
-        }
-    }
-
-    /**
-     * Schedules applying surface parameters on the next frame.
-     *
-     * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
-     *               this method to avoid synchronization issues.
-     */
-    public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) {
-        if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) {
-            return;
-        }
-
-        mPendingSequenceNumber++;
-        final int toApplySeqNo = mPendingSequenceNumber;
-        mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() {
-            @Override
-            public void onFrameDraw(long frame) {
-                if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
-                    Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
-                            .sendToTarget();
-                    return;
-                }
-                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame);
-                Transaction t = new Transaction();
-                for (int i = params.length - 1; i >= 0; i--) {
-                    SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams =
-                            params[i];
-                    surfaceParams.applyTo(t);
-                }
-                if (mTargetViewRootImpl != null) {
-                    mTargetViewRootImpl.mergeWithNextTransaction(t, frame);
-                } else {
-                    t.apply();
-                }
-                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
-                Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
-                        .sendToTarget();
-            }
-        });
-
-        // Make sure a frame gets scheduled.
-        mTargetViewRootImpl.getView().invalidate();
-    }
-
-    /**
-     * Calls the runnable when any pending apply calls have completed
-     */
-    public void addAfterApplyCallback(final Runnable afterApplyCallback) {
-        if (mSequenceNumber == mPendingSequenceNumber) {
-            afterApplyCallback.run();
-        } else {
-            if (mAfterApplyCallback == null) {
-                mAfterApplyCallback = afterApplyCallback;
-            } else {
-                final Runnable oldCallback = mAfterApplyCallback;
-                mAfterApplyCallback = new Runnable() {
-                    @Override
-                    public void run() {
-                        afterApplyCallback.run();
-                        oldCallback.run();
-                    }
-                };
-            }
-        }
-    }
-
-    public static void applyParams(TransactionCompat t,
-            SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) {
-        params.applyTo(t.mTransaction);
-    }
-
-    /**
-     * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
-     * attached if necessary.
-     */
-    public static void create(final View targetView,
-            final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) {
-        if (targetView == null) {
-            // No target view, no applier
-            callback.accept(null);
-        } else if (targetView.getViewRootImpl() != null) {
-            // Already attached, we're good to go
-            callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
-        } else {
-            // Haven't been attached before we can get the view root
-            targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
-                @Override
-                public void onViewAttachedToWindow(View v) {
-                    targetView.removeOnAttachStateChangeListener(this);
-                    callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
-                }
-
-                @Override
-                public void onViewDetachedFromWindow(View v) {
-                    // Do nothing
-                }
-            });
-        }
-    }
-
-    public static class SurfaceParams {
-        public static class Builder {
-            final SurfaceControl surface;
-            int flags;
-            float alpha;
-            float cornerRadius;
-            int backgroundBlurRadius;
-            Matrix matrix;
-            Rect windowCrop;
-            int layer;
-            SurfaceControl relativeTo;
-            int relativeLayer;
-            boolean visible;
-            float shadowRadius;
-
-            /**
-             * @param surface The surface to modify.
-             */
-            public Builder(SurfaceControl surface) {
-                this.surface = surface;
-            }
-
-            /**
-             * @param alpha The alpha value to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withAlpha(float alpha) {
-                this.alpha = alpha;
-                flags |= FLAG_ALPHA;
-                return this;
-            }
-
-            /**
-             * @param matrix The matrix to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withMatrix(Matrix matrix) {
-                this.matrix = new Matrix(matrix);
-                flags |= FLAG_MATRIX;
-                return this;
-            }
-
-            /**
-             * @param windowCrop The window crop to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withWindowCrop(Rect windowCrop) {
-                this.windowCrop = new Rect(windowCrop);
-                flags |= FLAG_WINDOW_CROP;
-                return this;
-            }
-
-            /**
-             * @param layer The layer to assign the surface.
-             * @return this Builder
-             */
-            public Builder withLayer(int layer) {
-                this.layer = layer;
-                flags |= FLAG_LAYER;
-                return this;
-            }
-
-            /**
-             * @param relativeTo The surface that's set relative layer to.
-             * @param relativeLayer The relative layer.
-             * @return this Builder
-             */
-            public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) {
-                this.relativeTo = relativeTo;
-                this.relativeLayer = relativeLayer;
-                flags |= FLAG_RELATIVE_LAYER;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for rounded corners to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withCornerRadius(float radius) {
-                this.cornerRadius = radius;
-                flags |= FLAG_CORNER_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for the shadows to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withShadowRadius(float radius) {
-                this.shadowRadius = radius;
-                flags |= FLAG_SHADOW_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for blur to apply to the background surfaces.
-             * @return this Builder
-             */
-            public Builder withBackgroundBlur(int radius) {
-                this.backgroundBlurRadius = radius;
-                flags |= FLAG_BACKGROUND_BLUR_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param visible The visibility to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withVisibility(boolean visible) {
-                this.visible = visible;
-                flags |= FLAG_VISIBILITY;
-                return this;
-            }
-
-            /**
-             * @return a new SurfaceParams instance
-             */
-            public SurfaceParams build() {
-                return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
-                        relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible,
-                        shadowRadius);
-            }
-        }
-
-        private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix,
-                Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer,
-                float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) {
-            this.flags = flags;
-            this.surface = surface;
-            this.alpha = alpha;
-            this.matrix = matrix;
-            this.windowCrop = windowCrop;
-            this.layer = layer;
-            this.relativeTo = relativeTo;
-            this.relativeLayer = relativeLayer;
-            this.cornerRadius = cornerRadius;
-            this.backgroundBlurRadius = backgroundBlurRadius;
-            this.visible = visible;
-            this.shadowRadius = shadowRadius;
-        }
-
-        private final int flags;
-        private final float[] mTmpValues = new float[9];
-
-        public final SurfaceControl surface;
-        public final float alpha;
-        public final float cornerRadius;
-        public final int backgroundBlurRadius;
-        public final Matrix matrix;
-        public final Rect windowCrop;
-        public final int layer;
-        public final SurfaceControl relativeTo;
-        public final int relativeLayer;
-        public final boolean visible;
-        public final float shadowRadius;
-
-        public void applyTo(SurfaceControl.Transaction t) {
-            if ((flags & FLAG_MATRIX) != 0) {
-                t.setMatrix(surface, matrix, mTmpValues);
-            }
-            if ((flags & FLAG_WINDOW_CROP) != 0) {
-                t.setWindowCrop(surface, windowCrop);
-            }
-            if ((flags & FLAG_ALPHA) != 0) {
-                t.setAlpha(surface, alpha);
-            }
-            if ((flags & FLAG_LAYER) != 0) {
-                t.setLayer(surface, layer);
-            }
-            if ((flags & FLAG_CORNER_RADIUS) != 0) {
-                t.setCornerRadius(surface, cornerRadius);
-            }
-            if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) {
-                t.setBackgroundBlurRadius(surface, backgroundBlurRadius);
-            }
-            if ((flags & FLAG_VISIBILITY) != 0) {
-                if (visible) {
-                    t.show(surface);
-                } else {
-                    t.hide(surface);
-                }
-            }
-            if ((flags & FLAG_RELATIVE_LAYER) != 0) {
-                t.setRelativeLayer(surface, relativeTo, relativeLayer);
-            }
-            if ((flags & FLAG_SHADOW_RADIUS) != 0) {
-                t.setShadowRadius(surface, shadowRadius);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
deleted file mode 100644
index 43a882a5..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2018 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.system;
-
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-
-public class TransactionCompat {
-
-    final Transaction mTransaction;
-
-    final float[] mTmpValues = new float[9];
-
-    public TransactionCompat() {
-        mTransaction = new Transaction();
-    }
-
-    public void apply() {
-        mTransaction.apply();
-    }
-
-    public TransactionCompat show(SurfaceControl surfaceControl) {
-        mTransaction.show(surfaceControl);
-        return this;
-    }
-
-    public TransactionCompat hide(SurfaceControl surfaceControl) {
-        mTransaction.hide(surfaceControl);
-        return this;
-    }
-
-    public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) {
-        mTransaction.setPosition(surfaceControl, x, y);
-        return this;
-    }
-
-    public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) {
-        mTransaction.setBufferSize(surfaceControl, w, h);
-        return this;
-    }
-
-    public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) {
-        mTransaction.setLayer(surfaceControl, z);
-        return this;
-    }
-
-    public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) {
-        mTransaction.setAlpha(surfaceControl, alpha);
-        return this;
-    }
-
-    public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) {
-        mTransaction.setOpaque(surfaceControl, opaque);
-        return this;
-    }
-
-    public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx,
-            float dtdy, float dsdy) {
-        mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy);
-        return this;
-    }
-
-    public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) {
-        mTransaction.setMatrix(surfaceControl, matrix, mTmpValues);
-        return this;
-    }
-
-    public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) {
-        mTransaction.setWindowCrop(surfaceControl, crop);
-        return this;
-    }
-
-    public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) {
-        mTransaction.setCornerRadius(surfaceControl, radius);
-        return this;
-    }
-
-    public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) {
-        mTransaction.setBackgroundBlurRadius(surfaceControl, radius);
-        return this;
-    }
-
-    public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) {
-        mTransaction.setColor(surfaceControl, color);
-        return this;
-    }
-
-    public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl,
-            SurfaceControl relativeTo, int z) {
-        t.setRelativeLayer(surfaceControl, relativeTo, z);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
index 0075ddd..450784e 100644
--- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
+++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
@@ -16,19 +16,29 @@
 
 package com.android.keyguard
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
 import android.content.Context
 import android.content.res.ColorStateList
 import android.content.res.TypedArray
 import android.graphics.Color
 import android.util.AttributeSet
+import android.view.View
 import com.android.settingslib.Utils
+import com.android.systemui.animation.Interpolators
 
 /** Displays security messages for the keyguard bouncer. */
-class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
+open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
     KeyguardMessageArea(context, attrs) {
     private val DEFAULT_COLOR = -1
     private var mDefaultColorState: ColorStateList? = null
     private var mNextMessageColorState: ColorStateList? = ColorStateList.valueOf(DEFAULT_COLOR)
+    private val animatorSet = AnimatorSet()
+    private var textAboutToShow: CharSequence? = null
+    protected open val SHOW_DURATION_MILLIS = 150L
+    protected open val HIDE_DURATION_MILLIS = 200L
 
     override fun updateTextColor() {
         var colorState = mDefaultColorState
@@ -58,4 +68,46 @@
         mDefaultColorState = Utils.getColorAttr(context, android.R.attr.textColorPrimary)
         super.reloadColor()
     }
+
+    override fun setMessage(msg: CharSequence?) {
+        if ((msg == textAboutToShow && msg != null) || msg == text) {
+            return
+        }
+        textAboutToShow = msg
+
+        if (animatorSet.isRunning) {
+            animatorSet.cancel()
+            textAboutToShow = null
+        }
+
+        val hideAnimator =
+            ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f).apply {
+                duration = HIDE_DURATION_MILLIS
+                interpolator = Interpolators.STANDARD_ACCELERATE
+            }
+
+        hideAnimator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    super@BouncerKeyguardMessageArea.setMessage(msg)
+                }
+            }
+        )
+        val showAnimator =
+            ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f).apply {
+                duration = SHOW_DURATION_MILLIS
+                interpolator = Interpolators.STANDARD_DECELERATE
+            }
+
+        showAnimator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    textAboutToShow = null
+                }
+            }
+        )
+
+        animatorSet.playSequentially(hideAnimator, showAnimator)
+        animatorSet.start()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 9151238..910955a 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -23,12 +23,18 @@
 import android.text.format.DateFormat
 import android.util.TypedValue
 import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.REGION_SAMPLING
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.ClockController
-import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shared.regionsampling.RegionSamplingInstance
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -38,13 +44,20 @@
 import java.util.TimeZone
 import java.util.concurrent.Executor
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
 
 /**
  * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
  * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
  */
 open class ClockEventController @Inject constructor(
-    private val statusBarStateController: StatusBarStateController,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val batteryController: BatteryController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -53,7 +66,7 @@
     private val context: Context,
     @Main private val mainExecutor: Executor,
     @Background private val bgExecutor: Executor,
-    private val featureFlags: FeatureFlags,
+    private val featureFlags: FeatureFlags
 ) {
     var clock: ClockController? = null
         set(value) {
@@ -70,9 +83,9 @@
     private var isCharging = false
     private var dozeAmount = 0f
     private var isKeyguardVisible = false
-
-    private val regionSamplingEnabled =
-            featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING)
+    private var isRegistered = false
+    private var disposableHandle: DisposableHandle? = null
+    private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
 
     private fun updateColors() {
         if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) {
@@ -165,15 +178,6 @@
         }
     }
 
-    private val statusBarStateListener = object : StatusBarStateController.StateListener {
-        override fun onDozeAmountChanged(linear: Float, eased: Float) {
-            clock?.animations?.doze(linear)
-
-            isDozing = linear > dozeAmount
-            dozeAmount = linear
-        }
-    }
-
     private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
         override fun onKeyguardVisibilityChanged(visible: Boolean) {
             isKeyguardVisible = visible
@@ -195,13 +199,11 @@
         }
     }
 
-    init {
-        isDozing = statusBarStateController.isDozing
-    }
-
-    fun registerListeners() {
-        dozeAmount = statusBarStateController.dozeAmount
-        isDozing = statusBarStateController.isDozing || dozeAmount != 0f
+    fun registerListeners(parent: View) {
+        if (isRegistered) {
+            return
+        }
+        isRegistered = true
 
         broadcastDispatcher.registerReceiver(
             localeBroadcastReceiver,
@@ -210,17 +212,28 @@
         configurationController.addCallback(configListener)
         batteryController.addCallback(batteryCallback)
         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
-        statusBarStateController.addCallback(statusBarStateListener)
         smallRegionSampler?.startRegionSampler()
         largeRegionSampler?.startRegionSampler()
+        disposableHandle = parent.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                listenForDozing(this)
+                listenForDozeAmount(this)
+                listenForDozeAmountTransition(this)
+            }
+        }
     }
 
     fun unregisterListeners() {
+        if (!isRegistered) {
+            return
+        }
+        isRegistered = false
+
+        disposableHandle?.dispose()
         broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
         configurationController.removeCallback(configListener)
         batteryController.removeCallback(batteryCallback)
         keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
-        statusBarStateController.removeCallback(statusBarStateListener)
         smallRegionSampler?.stopRegionSampler()
         largeRegionSampler?.stopRegionSampler()
     }
@@ -235,8 +248,39 @@
         largeRegionSampler?.dump(pw)
     }
 
-    companion object {
-        private val TAG = ClockEventController::class.simpleName
-        private const val FORMAT_NUMBER = 1234567890
+    @VisibleForTesting
+    internal fun listenForDozeAmount(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardInteractor.dozeAmount.collect {
+                dozeAmount = it
+                clock?.animations?.doze(dozeAmount)
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardTransitionInteractor.aodToLockscreenTransition.collect {
+                // Would eventually run this:
+                // dozeAmount = it.value
+                // clock?.animations?.doze(dozeAmount)
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal fun listenForDozing(scope: CoroutineScope): Job {
+        return scope.launch {
+            combine (
+                keyguardInteractor.dozeAmount,
+                keyguardInteractor.isDozing,
+            ) { localDozeAmount, localIsDozing ->
+                localDozeAmount > dozeAmount || localIsDozing
+            }
+            .collect { localIsDozing ->
+                isDozing = localIsDozing
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 20d064b..8eebe30 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -165,7 +165,7 @@
     protected void onViewAttached() {
         mClockRegistry.registerClockChangeListener(mClockChangedListener);
         setClock(mClockRegistry.createCurrentClock());
-        mClockEventController.registerListeners();
+        mClockEventController.registerListeners(mView);
         mKeyguardClockTopMargin =
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index f26b905..73229c3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -152,6 +152,7 @@
     }
 
     public void startAppearAnimation() {
+        mMessageAreaController.setMessage(getInitialMessageResId());
         mView.startAppearAnimation();
     }
 
@@ -169,6 +170,11 @@
         return view.indexOfChild(mView);
     }
 
+    /** Determines the message to show in the bouncer when it first appears. */
+    protected int getInitialMessageResId() {
+        return 0;
+    }
+
     /** Factory for a {@link KeyguardInputViewController}. */
     public static class Factory {
         private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
index c2802f7..2bd3ca5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
@@ -18,7 +18,6 @@
 
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
-import android.text.TextUtils;
 
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -100,15 +99,6 @@
         mView.setMessage(resId);
     }
 
-    /**
-     * Set Text if KeyguardMessageArea is empty.
-     */
-    public void setMessageIfEmpty(int resId) {
-        if (TextUtils.isEmpty(mView.getText())) {
-            setMessage(resId);
-        }
-    }
-
     public void setNextMessageColor(ColorStateList colorState) {
         mView.setNextMessageColor(colorState);
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 29e912f..0025986 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -187,7 +187,7 @@
     @Override
     void resetState() {
         mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser()));
-        mMessageAreaController.setMessage("");
+        mMessageAreaController.setMessage(getInitialMessageResId());
         final boolean wasDisabled = mPasswordEntry.isEnabled();
         mView.setPasswordEntryEnabled(true);
         mView.setPasswordEntryInputEnabled(true);
@@ -207,7 +207,6 @@
         if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) {
             showInput();
         }
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_password);
     }
 
     private void showInput() {
@@ -324,4 +323,9 @@
                 //enabled input method subtype (The current IME should be LatinIME.)
                 || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_password;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 9871645..1f0bd54 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -298,12 +298,6 @@
     }
 
     @Override
-    public void onResume(int reason) {
-        super.onResume(reason);
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pattern);
-    }
-
-    @Override
     public boolean needsInput() {
         return false;
     }
@@ -361,7 +355,7 @@
     }
 
     private void displayDefaultSecurityMessage() {
-        mMessageAreaController.setMessage("");
+        mMessageAreaController.setMessage(getInitialMessageResId());
     }
 
     private void handleAttemptLockout(long elapsedRealtimeDeadline) {
@@ -392,4 +386,9 @@
 
         }.start();
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_pattern;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 59a018a..f7423ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -127,7 +127,6 @@
     public void onResume(int reason) {
         super.onResume(reason);
         mPasswordEntry.requestFocus();
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index 89fcc47..7876f07 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -76,20 +76,13 @@
     }
 
     @Override
-    void resetState() {
-        super.resetState();
-        mMessageAreaController.setMessage("");
-    }
-
-    @Override
-    public void startAppearAnimation() {
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        super.startAppearAnimation();
-    }
-
-    @Override
     public boolean startDisappearAnimation(Runnable finishRunnable) {
         return mView.startDisappearAnimation(
                 mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable);
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_pin;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index bcd1a1e..81305f9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -219,13 +219,16 @@
     };
 
 
-    private SwipeListener mSwipeListener = new SwipeListener() {
+    private final SwipeListener mSwipeListener = new SwipeListener() {
         @Override
         public void onSwipeUp() {
             if (!mUpdateMonitor.isFaceDetectionRunning()) {
-                mUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+                boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true,
+                        FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
                 mKeyguardSecurityCallback.userActivity();
-                showMessage(null, null);
+                if (didFaceAuthRun) {
+                    showMessage(null, null);
+                }
             }
             if (mUpdateMonitor.isFaceEnrolled()) {
                 mUpdateMonitor.requestActiveUnlock(
@@ -234,7 +237,7 @@
             }
         }
     };
-    private ConfigurationController.ConfigurationListener mConfigurationListener =
+    private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
                 @Override
                 public void onThemeChanged() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index f558276..eab2a66 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -106,7 +106,6 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -2352,11 +2351,13 @@
      * @param userInitiatedRequest true if the user explicitly requested face auth
      * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being
      * invoked.
+     * @return current face auth detection state, true if it is running.
      */
-    public void requestFaceAuth(boolean userInitiatedRequest,
+    public boolean requestFaceAuth(boolean userInitiatedRequest,
             @FaceAuthApiRequestReason String reason) {
         mLogger.logFaceAuthRequested(userInitiatedRequest, reason);
         updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason));
+        return isFaceDetectionRunning();
     }
 
     /**
@@ -2366,10 +2367,6 @@
         stopListeningForFace(FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER);
     }
 
-    public boolean isFaceScanning() {
-        return mFaceRunningState == BIOMETRIC_STATE_RUNNING;
-    }
-
     private void updateFaceListeningState(int action, @NonNull FaceAuthUiEvent faceAuthUiEvent) {
         // If this message exists, we should not authenticate again until this message is
         // consumed by the handler
@@ -3796,4 +3793,17 @@
         }
         mListenModels.print(pw);
     }
+
+    /**
+     * Schedules a watchdog for the face and fingerprint BiometricScheduler.
+     * Cancels all operations in the scheduler if it is hung for 10 seconds.
+     */
+    public void startBiometricWatchdog() {
+        if (mFaceManager != null) {
+            mFaceManager.scheduleWatchdog();
+        }
+        if (mFpm != null) {
+            mFpm.scheduleWatchdog();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
similarity index 86%
rename from packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java
rename to packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
index c4be1ba535..72a44bd 100644
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
@@ -21,9 +21,14 @@
 import dagger.Module;
 import dagger.Provides;
 
-/** Dagger Module for clock package. */
+/**
+ * Dagger Module for clock package.
+ *
+ * @deprecated Migrate to ClockRegistry
+ */
 @Module
-public abstract class ClockModule {
+@Deprecated
+public abstract class ClockInfoModule {
 
     /** */
     @Provides
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
new file mode 100644
index 0000000..f43f559
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -0,0 +1,45 @@
+/*
+ * 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.keyguard.dagger;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.shared.clocks.ClockRegistry;
+import com.android.systemui.shared.clocks.DefaultClockProvider;
+import com.android.systemui.shared.plugins.PluginManager;
+
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger Module for clocks. */
+@Module
+public abstract class ClockRegistryModule {
+    /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */
+    @Provides
+    @SysUISingleton
+    public static ClockRegistry getClockRegistry(
+            @Application Context context,
+            PluginManager pluginManager,
+            @Main Handler handler,
+            DefaultClockProvider defaultClockProvider) {
+        return new ClockRegistry(context, pluginManager, handler, defaultClockProvider);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
index 2c2ab7b..6264ce7 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
@@ -17,9 +17,9 @@
 package com.android.keyguard.logging
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
 import com.android.systemui.log.dagger.BiometricMessagesLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
 import javax.inject.Inject
 
 /** Helper class for logging for [com.android.systemui.biometrics.FaceHelpMessageDeferral] */
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index 50012a5..46f3d4e 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -16,15 +16,15 @@
 
 package com.android.keyguard.logging
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.MessageInitializer
-import com.android.systemui.log.MessagePrinter
 import com.android.systemui.log.dagger.KeyguardLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.MessageInitializer
+import com.android.systemui.plugins.log.MessagePrinter
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 2eee957..82b32cf 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -22,13 +22,13 @@
 import com.android.keyguard.ActiveUnlockConfig
 import com.android.keyguard.KeyguardListenModel
 import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java
index 6525951..73fdce6 100644
--- a/packages/SystemUI/src/com/android/systemui/Dumpable.java
+++ b/packages/SystemUI/src/com/android/systemui/Dumpable.java
@@ -30,7 +30,6 @@
 
     /**
      * Called when it's time to dump the internal state
-     * @param fd A file descriptor.
      * @param pw Where to write your dump to.
      * @param args Arguments.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index c595586..3e0fa45 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -19,6 +19,7 @@
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
+import android.animation.TimeInterpolator
 import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Canvas
@@ -55,7 +56,7 @@
     private val rimRect = RectF()
     private var cameraProtectionColor = Color.BLACK
     var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
-            com.android.systemui.R.attr.wallpaperTextColorAccent)
+            R.attr.wallpaperTextColorAccent)
     private var cameraProtectionAnimator: ValueAnimator? = null
     var hideOverlayRunnable: Runnable? = null
     var faceAuthSucceeded = false
@@ -84,46 +85,19 @@
     }
 
     override fun drawCutoutProtection(canvas: Canvas) {
-        if (rimProgress > HIDDEN_RIM_SCALE && !protectionRect.isEmpty) {
-            val rimPath = Path(protectionPath)
-            val scaleMatrix = Matrix().apply {
-                val rimBounds = RectF()
-                rimPath.computeBounds(rimBounds, true)
-                setScale(rimProgress, rimProgress, rimBounds.centerX(), rimBounds.centerY())
-            }
-            rimPath.transform(scaleMatrix)
-            rimPaint.style = Paint.Style.FILL
-            val rimPaintAlpha = rimPaint.alpha
-            rimPaint.color = ColorUtils.blendARGB(
-                    faceScanningAnimColor,
-                    Color.WHITE,
-                    statusBarStateController.dozeAmount)
-            rimPaint.alpha = rimPaintAlpha
-            canvas.drawPath(rimPath, rimPaint)
+        if (protectionRect.isEmpty) {
+            return
         }
-
-        if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE &&
-                !protectionRect.isEmpty) {
-            val scaledProtectionPath = Path(protectionPath)
-            val scaleMatrix = Matrix().apply {
-                val protectionPathRect = RectF()
-                scaledProtectionPath.computeBounds(protectionPathRect, true)
-                setScale(cameraProtectionProgress, cameraProtectionProgress,
-                        protectionPathRect.centerX(), protectionPathRect.centerY())
-            }
-            scaledProtectionPath.transform(scaleMatrix)
-            paint.style = Paint.Style.FILL
-            paint.color = cameraProtectionColor
-            canvas.drawPath(scaledProtectionPath, paint)
+        if (rimProgress > HIDDEN_RIM_SCALE) {
+            drawFaceScanningRim(canvas)
         }
-    }
-
-    override fun updateVisOnUpdateCutout(): Boolean {
-        return false // instead, we always update the visibility whenever face scanning starts/ends
+        if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) {
+            drawCameraProtection(canvas)
+        }
     }
 
     override fun enableShowProtection(show: Boolean) {
-        val showScanningAnimNow = keyguardUpdateMonitor.isFaceScanning && show
+        val showScanningAnimNow = keyguardUpdateMonitor.isFaceDetectionRunning && show
         if (showScanningAnimNow == showScanningAnim) {
             return
         }
@@ -152,91 +126,26 @@
                     if (showScanningAnim) Interpolators.STANDARD_ACCELERATE
                     else if (faceAuthSucceeded) Interpolators.STANDARD
                     else Interpolators.STANDARD_DECELERATE
-            addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                animation: ValueAnimator ->
-                cameraProtectionProgress = animation.animatedValue as Float
-                invalidate()
-            })
+            addUpdateListener(this@FaceScanningOverlay::updateCameraProtectionProgress)
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
                     cameraProtectionAnimator = null
                     if (!showScanningAnim) {
-                        visibility = View.INVISIBLE
-                        hideOverlayRunnable?.run()
-                        hideOverlayRunnable = null
-                        requestLayout()
+                        hide()
                     }
                 }
             })
         }
 
         rimAnimator?.cancel()
-        rimAnimator = AnimatorSet().apply {
-            if (showScanningAnim) {
-                val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE,
-                        PULSE_RADIUS_OUT).apply {
-                    duration = PULSE_APPEAR_DURATION
-                    interpolator = Interpolators.STANDARD_DECELERATE
-                    addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                        animation: ValueAnimator ->
-                        rimProgress = animation.animatedValue as Float
-                        invalidate()
-                    })
-                }
-
-                // animate in camera protection, rim, and then pulse in/out
-                playSequentially(cameraProtectionAnimator, rimAppearAnimator,
-                        createPulseAnimator(), createPulseAnimator(),
-                        createPulseAnimator(), createPulseAnimator(),
-                        createPulseAnimator(), createPulseAnimator())
-            } else {
-                val rimDisappearAnimator = ValueAnimator.ofFloat(
-                        rimProgress,
-                        if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS
-                        else SHOW_CAMERA_PROTECTION_SCALE
-                ).apply {
-                    duration =
-                            if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
-                            else PULSE_ERROR_DISAPPEAR_DURATION
-                    interpolator =
-                            if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE
-                            else Interpolators.STANDARD
-                    addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                        animation: ValueAnimator ->
-                        rimProgress = animation.animatedValue as Float
-                        invalidate()
-                    })
-                    addListener(object : AnimatorListenerAdapter() {
-                        override fun onAnimationEnd(animation: Animator) {
-                            rimProgress = HIDDEN_RIM_SCALE
-                            invalidate()
-                        }
-                    })
-                }
-                if (faceAuthSucceeded) {
-                    val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply {
-                        duration = PULSE_SUCCESS_DISAPPEAR_DURATION
-                        interpolator = Interpolators.LINEAR
-                        addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                            animation: ValueAnimator ->
-                            rimPaint.alpha = animation.animatedValue as Int
-                            invalidate()
-                        })
-                        addListener(object : AnimatorListenerAdapter() {
-                            override fun onAnimationEnd(animation: Animator) {
-                                rimPaint.alpha = 255
-                                invalidate()
-                            }
-                        })
-                    }
-                    val rimSuccessAnimator = AnimatorSet()
-                    rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator)
-                    playTogether(rimSuccessAnimator, cameraProtectionAnimator)
-                } else {
-                    playTogether(rimDisappearAnimator, cameraProtectionAnimator)
-                }
-            }
-
+        rimAnimator = if (showScanningAnim) {
+            createFaceScanningRimAnimator()
+        } else if (faceAuthSucceeded) {
+            createFaceSuccessRimAnimator()
+        } else {
+            createFaceNotSuccessRimAnimator()
+        }
+        rimAnimator?.apply {
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
                     rimAnimator = null
@@ -245,34 +154,12 @@
                     }
                 }
             })
-            start()
         }
+        rimAnimator?.start()
     }
 
-    fun createPulseAnimator(): AnimatorSet {
-        return AnimatorSet().apply {
-            val pulseInwards = ValueAnimator.ofFloat(
-                    PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply {
-                duration = PULSE_DURATION_INWARDS
-                interpolator = Interpolators.STANDARD
-                addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                    animation: ValueAnimator ->
-                    rimProgress = animation.animatedValue as Float
-                    invalidate()
-                })
-            }
-            val pulseOutwards = ValueAnimator.ofFloat(
-                    PULSE_RADIUS_IN, PULSE_RADIUS_OUT).apply {
-                duration = PULSE_DURATION_OUTWARDS
-                interpolator = Interpolators.STANDARD
-                addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                    animation: ValueAnimator ->
-                    rimProgress = animation.animatedValue as Float
-                    invalidate()
-                })
-            }
-            playSequentially(pulseInwards, pulseOutwards)
-        }
+    override fun updateVisOnUpdateCutout(): Boolean {
+        return false // instead, we always update the visibility whenever face scanning starts/ends
     }
 
     override fun updateProtectionBoundingPath() {
@@ -290,17 +177,153 @@
             // Make sure that our measured height encompasses the extra space for the animation
             mTotalBounds.union(mBoundingRect)
             mTotalBounds.union(
-                    rimRect.left.toInt(),
-                    rimRect.top.toInt(),
-                    rimRect.right.toInt(),
-                    rimRect.bottom.toInt())
+                rimRect.left.toInt(),
+                rimRect.top.toInt(),
+                rimRect.right.toInt(),
+                rimRect.bottom.toInt())
             setMeasuredDimension(
-                    resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
-                    resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
+                resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
+                resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
         } else {
             setMeasuredDimension(
-                    resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
-                    resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0))
+                resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
+                resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0))
+        }
+    }
+
+    private fun drawFaceScanningRim(canvas: Canvas) {
+        val rimPath = Path(protectionPath)
+        scalePath(rimPath, rimProgress)
+        rimPaint.style = Paint.Style.FILL
+        val rimPaintAlpha = rimPaint.alpha
+        rimPaint.color = ColorUtils.blendARGB(
+            faceScanningAnimColor,
+            Color.WHITE,
+            statusBarStateController.dozeAmount
+        )
+        rimPaint.alpha = rimPaintAlpha
+        canvas.drawPath(rimPath, rimPaint)
+    }
+
+    private fun drawCameraProtection(canvas: Canvas) {
+        val scaledProtectionPath = Path(protectionPath)
+        scalePath(scaledProtectionPath, cameraProtectionProgress)
+        paint.style = Paint.Style.FILL
+        paint.color = cameraProtectionColor
+        canvas.drawPath(scaledProtectionPath, paint)
+    }
+
+    private fun createFaceSuccessRimAnimator(): AnimatorSet {
+        val rimSuccessAnimator = AnimatorSet()
+        rimSuccessAnimator.playTogether(
+            createRimDisappearAnimator(
+                PULSE_RADIUS_SUCCESS,
+                PULSE_SUCCESS_DISAPPEAR_DURATION,
+                Interpolators.STANDARD_DECELERATE
+            ),
+            createSuccessOpacityAnimator(),
+        )
+        return AnimatorSet().apply {
+            playTogether(rimSuccessAnimator, cameraProtectionAnimator)
+        }
+    }
+
+    private fun createFaceNotSuccessRimAnimator(): AnimatorSet {
+        return AnimatorSet().apply {
+            playTogether(
+                createRimDisappearAnimator(
+                    SHOW_CAMERA_PROTECTION_SCALE,
+                    PULSE_ERROR_DISAPPEAR_DURATION,
+                    Interpolators.STANDARD
+                ),
+                cameraProtectionAnimator,
+            )
+        }
+    }
+
+    private fun createRimDisappearAnimator(
+        endValue: Float,
+        animDuration: Long,
+        timeInterpolator: TimeInterpolator
+    ): ValueAnimator {
+        return ValueAnimator.ofFloat(rimProgress, endValue).apply {
+            duration = animDuration
+            interpolator = timeInterpolator
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
+            addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    rimProgress = HIDDEN_RIM_SCALE
+                    invalidate()
+                }
+            })
+        }
+    }
+
+    private fun createSuccessOpacityAnimator(): ValueAnimator {
+        return ValueAnimator.ofInt(255, 0).apply {
+            duration = PULSE_SUCCESS_DISAPPEAR_DURATION
+            interpolator = Interpolators.LINEAR
+            addUpdateListener(this@FaceScanningOverlay::updateRimAlpha)
+            addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    rimPaint.alpha = 255
+                    invalidate()
+                }
+            })
+        }
+    }
+
+    private fun createFaceScanningRimAnimator(): AnimatorSet {
+        return AnimatorSet().apply {
+            playSequentially(
+                cameraProtectionAnimator,
+                createRimAppearAnimator(),
+                createPulseAnimator()
+            )
+        }
+    }
+
+    private fun createRimAppearAnimator(): ValueAnimator {
+        return ValueAnimator.ofFloat(
+            SHOW_CAMERA_PROTECTION_SCALE,
+            PULSE_RADIUS_OUT
+        ).apply {
+            duration = PULSE_APPEAR_DURATION
+            interpolator = Interpolators.STANDARD_DECELERATE
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
+        }
+    }
+
+    private fun hide() {
+        visibility = INVISIBLE
+        hideOverlayRunnable?.run()
+        hideOverlayRunnable = null
+        requestLayout()
+    }
+
+    private fun updateRimProgress(animator: ValueAnimator) {
+        rimProgress = animator.animatedValue as Float
+        invalidate()
+    }
+
+    private fun updateCameraProtectionProgress(animator: ValueAnimator) {
+        cameraProtectionProgress = animator.animatedValue as Float
+        invalidate()
+    }
+
+    private fun updateRimAlpha(animator: ValueAnimator) {
+        rimPaint.alpha = animator.animatedValue as Int
+        invalidate()
+    }
+
+    private fun createPulseAnimator(): ValueAnimator {
+        return ValueAnimator.ofFloat(
+                PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply {
+            duration = HALF_PULSE_DURATION
+            interpolator = Interpolators.STANDARD
+            repeatCount = 11 // Pulse inwards and outwards, reversing direction, 6 times
+            repeatMode = ValueAnimator.REVERSE
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
         }
     }
 
@@ -363,13 +386,24 @@
         private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L
         private const val PULSE_APPEAR_DURATION = 250L // without start delay
 
-        private const val PULSE_DURATION_INWARDS = 500L
-        private const val PULSE_DURATION_OUTWARDS = 500L
+        private const val HALF_PULSE_DURATION = 500L
 
         private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L
         private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay
 
         private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L
         private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay
+
+        private fun scalePath(path: Path, scalingFactor: Float) {
+            val scaleMatrix = Matrix().apply {
+                val boundingRectangle = RectF()
+                path.computeBounds(boundingRectangle, true)
+                setScale(
+                    scalingFactor, scalingFactor,
+                    boundingRectangle.centerX(), boundingRectangle.centerY()
+                )
+            }
+            path.transform(scaleMatrix)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt
new file mode 100644
index 0000000..4c3a7ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt
@@ -0,0 +1,7 @@
+package com.android.systemui
+
+import com.android.systemui.dump.nano.SystemUIProtoDump
+
+interface ProtoDumpable : Dumpable {
+    fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index 7bcba3c..50e0399 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -121,6 +121,6 @@
                     DumpHandler.PRIORITY_ARG_CRITICAL};
         }
 
-        mDumpHandler.dump(pw, massagedArgs);
+        mDumpHandler.dump(fd, pw, massagedArgs);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
new file mode 100644
index 0000000..d6d03990
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -0,0 +1,378 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static android.util.MathUtils.constrain;
+
+import static java.util.Objects.requireNonNull;
+
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FlingAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.HashMap;
+
+/**
+ * Controls the interaction animations of the {@link MenuView}. Also, it will use the relative
+ * coordinate based on the {@link MenuViewLayer} to compute the offset of the {@link MenuView}.
+ */
+class MenuAnimationController {
+    private static final String TAG = "MenuAnimationController";
+    private static final boolean DEBUG = false;
+    private static final float MIN_PERCENT = 0.0f;
+    private static final float MAX_PERCENT = 1.0f;
+    private static final float COMPLETELY_OPAQUE = 1.0f;
+    private static final float FLING_FRICTION_SCALAR = 1.9f;
+    private static final float DEFAULT_FRICTION = 4.2f;
+    private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
+    private static final float SPRING_STIFFNESS = 700f;
+    private static final float ESCAPE_VELOCITY = 750f;
+
+    private static final int FADE_OUT_DURATION_MS = 1000;
+    private static final int FADE_EFFECT_DURATION_MS = 3000;
+
+    private final MenuView mMenuView;
+    private final ValueAnimator mFadeOutAnimator;
+    private final Handler mHandler;
+    private boolean mIsMovedToEdge;
+    private boolean mIsFadeEffectEnabled;
+
+    // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link
+    // DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler
+    private final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations =
+            new HashMap<>();
+
+    MenuAnimationController(MenuView menuView) {
+        mMenuView = menuView;
+
+        mHandler = createUiHandler();
+        mFadeOutAnimator = new ValueAnimator();
+        mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
+        mFadeOutAnimator.addUpdateListener(
+                (animation) -> menuView.setAlpha((float) animation.getAnimatedValue()));
+    }
+
+    void moveToPosition(PointF position) {
+        moveToPositionX(position.x);
+        moveToPositionY(position.y);
+    }
+
+    void moveToPositionX(float positionX) {
+        DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX);
+    }
+
+    private void moveToPositionY(float positionY) {
+        DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY);
+    }
+
+    void moveToPositionYIfNeeded(float positionY) {
+        // If the list view was out of screen bounds, it would allow users to nest scroll inside
+        // and avoid conflicting with outer scroll.
+        final RecyclerView listView = (RecyclerView) mMenuView.getChildAt(/* index= */ 0);
+        if (listView.getOverScrollMode() == View.OVER_SCROLL_NEVER) {
+            moveToPositionY(positionY);
+        }
+    }
+
+    void moveToTopLeftPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.top));
+    }
+
+    void moveToTopRightPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.top));
+    }
+
+    void moveToBottomLeftPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.bottom));
+    }
+
+    void moveToBottomRightPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.bottom));
+    }
+
+    void moveAndPersistPosition(PointF position) {
+        moveToPosition(position);
+        mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
+        constrainPositionAndUpdate(position);
+    }
+
+    void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) {
+        final boolean shouldMenuFlingLeft = isOnLeftSide()
+                ? velocityX < ESCAPE_VELOCITY
+                : velocityX < -ESCAPE_VELOCITY;
+
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        final float finalPositionX = shouldMenuFlingLeft
+                ? draggableBounds.left : draggableBounds.right;
+
+        final float minimumVelocityToReachEdge =
+                (finalPositionX - x) * (FLING_FRICTION_SCALAR * DEFAULT_FRICTION);
+
+        final float startXVelocity = shouldMenuFlingLeft
+                ? Math.min(minimumVelocityToReachEdge, velocityX)
+                : Math.max(minimumVelocityToReachEdge, velocityX);
+
+        flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
+                startXVelocity,
+                FLING_FRICTION_SCALAR,
+                new SpringForce()
+                        .setStiffness(SPRING_STIFFNESS)
+                        .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+                finalPositionX);
+
+        flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_Y,
+                velocityY,
+                FLING_FRICTION_SCALAR,
+                new SpringForce()
+                        .setStiffness(SPRING_STIFFNESS)
+                        .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+                /* finalPosition= */ null);
+    }
+
+    private void flingThenSpringMenuWith(DynamicAnimation.ViewProperty property, float velocity,
+            float friction, SpringForce spring, Float finalPosition) {
+
+        final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
+        final float currentValue = menuPositionProperty.getValue(mMenuView);
+        final Rect bounds = mMenuView.getMenuDraggableBounds();
+        final float min =
+                property.equals(DynamicAnimation.TRANSLATION_X)
+                        ? bounds.left
+                        : bounds.top;
+        final float max =
+                property.equals(DynamicAnimation.TRANSLATION_X)
+                        ? bounds.right
+                        : bounds.bottom;
+
+        final FlingAnimation flingAnimation = new FlingAnimation(mMenuView, menuPositionProperty);
+        flingAnimation.setFriction(friction)
+                .setStartVelocity(velocity)
+                .setMinValue(Math.min(currentValue, min))
+                .setMaxValue(Math.max(currentValue, max))
+                .addEndListener((animation, canceled, endValue, endVelocity) -> {
+                    if (canceled) {
+                        if (DEBUG) {
+                            Log.d(TAG, "The fling animation was canceled.");
+                        }
+
+                        return;
+                    }
+
+                    final float endPosition = finalPosition != null
+                            ? finalPosition
+                            : Math.max(min, Math.min(max, endValue));
+                    springMenuWith(property, spring, endVelocity, endPosition);
+                });
+
+        cancelAnimation(property);
+        mPositionAnimations.put(property, flingAnimation);
+        flingAnimation.start();
+    }
+
+    private void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
+            float velocity, float finalPosition) {
+        final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
+        final SpringAnimation springAnimation =
+                new SpringAnimation(mMenuView, menuPositionProperty)
+                        .setSpring(spring)
+                        .addEndListener((animation, canceled, endValue, endVelocity) -> {
+                            if (canceled || endValue != finalPosition) {
+                                return;
+                            }
+
+                            onSpringAnimationEnd(new PointF(mMenuView.getTranslationX(),
+                                    mMenuView.getTranslationY()));
+                        })
+                        .setStartVelocity(velocity);
+
+        cancelAnimation(property);
+        mPositionAnimations.put(property, springAnimation);
+        springAnimation.animateToFinalPosition(finalPosition);
+    }
+
+    /**
+     * Determines whether to hide the menu to the edge of the screen with the given current
+     * translation x of the menu view. It should be used when receiving the action up touch event.
+     *
+     * @param currentXTranslation the current translation x of the menu view.
+     * @return true if the menu would be hidden to the edge, otherwise false.
+     */
+    boolean maybeMoveToEdgeAndHide(float currentXTranslation) {
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+
+        // If the translation x is zero, it should be at the left of the bound.
+        if (currentXTranslation < draggableBounds.left
+                || currentXTranslation > draggableBounds.right) {
+            moveToEdgeAndHide();
+            return true;
+        }
+
+        fadeOutIfEnabled();
+        return false;
+    }
+
+    private boolean isOnLeftSide() {
+        return mMenuView.getTranslationX() < mMenuView.getMenuDraggableBounds().centerX();
+    }
+
+    boolean isMovedToEdge() {
+        return mIsMovedToEdge;
+    }
+
+    void moveToEdgeAndHide() {
+        mIsMovedToEdge = true;
+
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        final float endY = constrain(mMenuView.getTranslationY(), draggableBounds.top,
+                draggableBounds.bottom);
+        final float menuHalfWidth = mMenuView.getWidth() / 2.0f;
+        final float endX = isOnLeftSide()
+                ? draggableBounds.left - menuHalfWidth
+                : draggableBounds.right + menuHalfWidth;
+        moveAndPersistPosition(new PointF(endX, endY));
+
+        // Keep the touch region let users could click extra space to pop up the menu view
+        // from the screen edge
+        mMenuView.onBoundsInParentChanged(isOnLeftSide()
+                ? draggableBounds.left
+                : draggableBounds.right, (int) mMenuView.getTranslationY());
+
+        fadeOutIfEnabled();
+    }
+
+    void moveOutEdgeAndShow() {
+        mIsMovedToEdge = false;
+
+        mMenuView.onPositionChanged();
+        mMenuView.onEdgeChangedIfNeeded();
+    }
+
+    void cancelAnimations() {
+        cancelAnimation(DynamicAnimation.TRANSLATION_X);
+        cancelAnimation(DynamicAnimation.TRANSLATION_Y);
+    }
+
+    private void cancelAnimation(DynamicAnimation.ViewProperty property) {
+        if (!mPositionAnimations.containsKey(property)) {
+            return;
+        }
+
+        mPositionAnimations.get(property).cancel();
+    }
+
+    void onDraggingStart() {
+        mMenuView.onDraggingStart();
+    }
+
+    private void onSpringAnimationEnd(PointF position) {
+        mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
+        constrainPositionAndUpdate(position);
+
+        fadeOutIfEnabled();
+    }
+
+    private void constrainPositionAndUpdate(PointF position) {
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        // Have the space gap margin between the top bound and the menu view, so actually the
+        // position y range needs to cut the margin.
+        position.offset(-draggableBounds.left, -draggableBounds.top);
+
+        final float percentageX = position.x < draggableBounds.centerX()
+                ? MIN_PERCENT : MAX_PERCENT;
+
+        final float percentageY = position.y < 0 || draggableBounds.height() == 0
+                ? MIN_PERCENT
+                : Math.min(MAX_PERCENT, position.y / draggableBounds.height());
+        mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+    }
+
+    void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
+        mIsFadeEffectEnabled = isFadeEffectEnabled;
+
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+        mFadeOutAnimator.cancel();
+        mFadeOutAnimator.setFloatValues(COMPLETELY_OPAQUE, newOpacityValue);
+        mHandler.post(() -> mMenuView.setAlpha(
+                mIsFadeEffectEnabled ? newOpacityValue : COMPLETELY_OPAQUE));
+    }
+
+    void fadeInNowIfEnabled() {
+        if (!mIsFadeEffectEnabled) {
+            return;
+        }
+
+        cancelAndRemoveCallbacksAndMessages();
+        mHandler.post(() -> mMenuView.setAlpha(COMPLETELY_OPAQUE));
+    }
+
+    void fadeOutIfEnabled() {
+        if (!mIsFadeEffectEnabled) {
+            return;
+        }
+
+        cancelAndRemoveCallbacksAndMessages();
+        mHandler.postDelayed(mFadeOutAnimator::start, FADE_EFFECT_DURATION_MS);
+    }
+
+    private void cancelAndRemoveCallbacksAndMessages() {
+        mFadeOutAnimator.cancel();
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+    }
+
+    private Handler createUiHandler() {
+        return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
+    }
+
+    static class MenuPositionProperty
+            extends FloatPropertyCompat<MenuView> {
+        private final DynamicAnimation.ViewProperty mProperty;
+
+        MenuPositionProperty(DynamicAnimation.ViewProperty property) {
+            super(property.toString());
+            mProperty = property;
+        }
+
+        @Override
+        public float getValue(MenuView menuView) {
+            return mProperty.getValue(menuView);
+        }
+
+        @Override
+        public void setValue(MenuView menuView, float value) {
+            mProperty.setValue(menuView, value);
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt
similarity index 61%
copy from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
copy to packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt
index 23072a2..83c344c 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt
@@ -14,8 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.gallery
+package com.android.systemui.accessibility.floatingmenu
 
-import com.android.settingslib.spa.framework.DebugActivity
+import android.annotation.FloatRange
 
-class GalleryDebugActivity : DebugActivity()
+@FloatRange(from = 0.0, to = 1.0) const val DEFAULT_OPACITY_VALUE = 0.55f
+const val DEFAULT_FADE_EFFECT_IS_ENABLED = 1
+
+/** The data class for the fade effect info of the accessibility floating menu view. */
+data class MenuFadeEffectInfo(val isFadeEffectEnabled: Boolean, val opacity: Float)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
index 698d60a..57019de 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
@@ -16,22 +16,29 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
+import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
 import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
 import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
 
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
+import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_FADE_EFFECT_IS_ENABLED;
+import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_OPACITY_VALUE;
 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
 
+import android.annotation.FloatRange;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Prefs;
 
 import java.util.List;
 
@@ -39,9 +46,16 @@
  * Stores and observe the settings contents for the menu view.
  */
 class MenuInfoRepository {
+    @FloatRange(from = 0.0, to = 1.0)
+    private static final float DEFAULT_MENU_POSITION_X_PERCENT = 1.0f;
+
+    @FloatRange(from = 0.0, to = 1.0)
+    private static final float DEFAULT_MENU_POSITION_Y_PERCENT = 0.9f;
+
     private final Context mContext;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final OnSettingsContentsChanged mSettingsContentsCallback;
+    private Position mPercentagePosition;
 
     private final ContentObserver mMenuTargetFeaturesContentObserver =
             new ContentObserver(mHandler) {
@@ -62,9 +76,24 @@
                 }
             };
 
+    @VisibleForTesting
+    final ContentObserver mMenuFadeOutContentObserver =
+            new ContentObserver(mHandler) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    mSettingsContentsCallback.onFadeEffectInfoChanged(getMenuFadeEffectInfo());
+                }
+            };
+
     MenuInfoRepository(Context context, OnSettingsContentsChanged settingsContentsChanged) {
         mContext = context;
         mSettingsContentsCallback = settingsContentsChanged;
+
+        mPercentagePosition = getStartPosition();
+    }
+
+    void loadMenuPosition(OnInfoReady<Position> callback) {
+        callback.onReady(mPercentagePosition);
     }
 
     void loadMenuTargetFeatures(OnInfoReady<List<AccessibilityTarget>> callback) {
@@ -75,6 +104,30 @@
         callback.onReady(getMenuSizeTypeFromSettings(mContext));
     }
 
+    void loadMenuFadeEffectInfo(OnInfoReady<MenuFadeEffectInfo> callback) {
+        callback.onReady(getMenuFadeEffectInfo());
+    }
+
+    private MenuFadeEffectInfo getMenuFadeEffectInfo() {
+        return new MenuFadeEffectInfo(isMenuFadeEffectEnabledFromSettings(mContext),
+                getMenuOpacityFromSettings(mContext));
+    }
+
+    void updateMenuSavingPosition(Position percentagePosition) {
+        mPercentagePosition = percentagePosition;
+        Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
+                percentagePosition.toString());
+    }
+
+    private Position getStartPosition() {
+        final String absolutePositionString = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
+
+        return TextUtils.isEmpty(absolutePositionString)
+                ? new Position(DEFAULT_MENU_POSITION_X_PERCENT, DEFAULT_MENU_POSITION_Y_PERCENT)
+                : Position.fromString(absolutePositionString);
+    }
+
     void registerContentObservers() {
         mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
@@ -88,17 +141,28 @@
                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE),
                 /* notifyForDescendants */ false, mMenuSizeContentObserver,
                 UserHandle.USER_CURRENT);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED),
+                /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
+                UserHandle.USER_CURRENT);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_OPACITY),
+                /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
+                UserHandle.USER_CURRENT);
     }
 
     void unregisterContentObservers() {
         mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver);
         mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver);
+        mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver);
     }
 
     interface OnSettingsContentsChanged {
         void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures);
 
         void onSizeTypeChanged(int newSizeType);
+
+        void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo);
     }
 
     interface OnInfoReady<T> {
@@ -109,4 +173,16 @@
         return Settings.Secure.getIntForUser(context.getContentResolver(),
                 ACCESSIBILITY_FLOATING_MENU_SIZE, SMALL, UserHandle.USER_CURRENT);
     }
+
+    private static boolean isMenuFadeEffectEnabledFromSettings(Context context) {
+        return Settings.Secure.getIntForUser(context.getContentResolver(),
+                ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
+                DEFAULT_FADE_EFFECT_IS_ENABLED, UserHandle.USER_CURRENT) == /* enabled */ 1;
+    }
+
+    private static float getMenuOpacityFromSettings(Context context) {
+        return Settings.Secure.getFloatForUser(context.getContentResolver(),
+                ACCESSIBILITY_FLOATING_MENU_OPACITY, DEFAULT_OPACITY_VALUE,
+                UserHandle.USER_CURRENT);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
new file mode 100644
index 0000000..e69a248
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.floatingmenu;
+
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+
+import com.android.systemui.R;
+
+/**
+ * An accessibility item delegate for the individual items of the list view in the
+ * {@link MenuView}.
+ */
+class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.ItemDelegate {
+    private final MenuAnimationController mAnimationController;
+
+    MenuItemAccessibilityDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate,
+            MenuAnimationController animationController) {
+        super(recyclerViewDelegate);
+        mAnimationController = animationController;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+        super.onInitializeAccessibilityNodeInfo(host, info);
+
+        final Resources res = host.getResources();
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_top_left));
+        info.addAction(moveTopLeft);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_top_right,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_top_right));
+        info.addAction(moveTopRight);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_bottom_left,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_bottom_left));
+        info.addAction(moveBottomLeft);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_bottom_right,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_bottom_right));
+        info.addAction(moveBottomRight);
+
+        final int moveEdgeId = mAnimationController.isMovedToEdge()
+                ? R.id.action_move_out_edge_and_show
+                : R.id.action_move_to_edge_and_hide;
+        final int moveEdgeTextResId = mAnimationController.isMovedToEdge()
+                ? R.string.accessibility_floating_button_action_move_out_edge_and_show
+                : R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half;
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId,
+                        res.getString(moveEdgeTextResId));
+        info.addAction(moveToOrOutEdge);
+    }
+
+    @Override
+    public boolean performAccessibilityAction(View host, int action, Bundle args) {
+        if (action == ACTION_ACCESSIBILITY_FOCUS) {
+            mAnimationController.fadeInNowIfEnabled();
+        }
+
+        if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
+            mAnimationController.fadeOutIfEnabled();
+        }
+
+        if (action == R.id.action_move_top_left) {
+            mAnimationController.moveToTopLeftPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_top_right) {
+            mAnimationController.moveToTopRightPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_bottom_left) {
+            mAnimationController.moveToBottomLeftPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_bottom_right) {
+            mAnimationController.moveToBottomRightPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_to_edge_and_hide) {
+            mAnimationController.moveToEdgeAndHide();
+            return true;
+        }
+
+        if (action == R.id.action_move_out_edge_and_show) {
+            mAnimationController.moveOutEdgeAndShow();
+            return true;
+        }
+
+        return super.performAccessibilityAction(host, action, args);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
new file mode 100644
index 0000000..3146c9f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
@@ -0,0 +1,121 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Controls the all touch events of the accessibility target features view{@link RecyclerView} in
+ * the {@link MenuView}. And then compute the gestures' velocity for fling and spring
+ * animations.
+ */
+class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
+    private static final int VELOCITY_UNIT_SECONDS = 1000;
+    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+    private final MenuAnimationController mMenuAnimationController;
+    private final PointF mDown = new PointF();
+    private final PointF mMenuTranslationDown = new PointF();
+    private boolean mIsDragging = false;
+    private float mTouchSlop;
+
+    MenuListViewTouchHandler(MenuAnimationController menuAnimationController) {
+        mMenuAnimationController = menuAnimationController;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
+            @NonNull MotionEvent motionEvent) {
+
+        final View menuView = (View) recyclerView.getParent();
+        addMovement(motionEvent);
+
+        final float dx = motionEvent.getRawX() - mDown.x;
+        final float dy = motionEvent.getRawY() - mDown.y;
+
+        switch (motionEvent.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mMenuAnimationController.fadeInNowIfEnabled();
+                mTouchSlop = ViewConfiguration.get(recyclerView.getContext()).getScaledTouchSlop();
+                mDown.set(motionEvent.getRawX(), motionEvent.getRawY());
+                mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY());
+
+                mMenuAnimationController.cancelAnimations();
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) {
+                    if (!mIsDragging) {
+                        mIsDragging = true;
+                        mMenuAnimationController.onDraggingStart();
+                    }
+
+                    mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx);
+                    mMenuAnimationController.moveToPositionYIfNeeded(mMenuTranslationDown.y + dy);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsDragging) {
+                    final float endX = mMenuTranslationDown.x + dx;
+                    mIsDragging = false;
+
+                    if (!mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
+                        mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS);
+                        mMenuAnimationController.flingMenuThenSpringToEdge(endX,
+                                mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+                    }
+
+                    // Avoid triggering the listener of the item.
+                    return true;
+                }
+
+                break;
+            default: // Do nothing
+        }
+
+        // not consume all the events here because keeping the scroll behavior of list view.
+        return false;
+    }
+
+    @Override
+    public void onTouchEvent(@NonNull RecyclerView recyclerView,
+            @NonNull MotionEvent motionEvent) {
+        // Do nothing
+    }
+
+    @Override
+    public void onRequestDisallowInterceptTouchEvent(boolean b) {
+        // Do nothing
+    }
+
+    /**
+     * Adds a movement to the velocity tracker using raw screen coordinates.
+     */
+    private void addMovement(MotionEvent motionEvent) {
+        final float deltaX = motionEvent.getRawX() - motionEvent.getX();
+        final float deltaY = motionEvent.getRawY() - motionEvent.getY();
+        motionEvent.offsetLocation(deltaX, deltaY);
+        mVelocityTracker.addMovement(motionEvent);
+        motionEvent.offsetLocation(-deltaX, -deltaY);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index 576f23e..15d139c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -21,28 +21,44 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.lifecycle.Observer;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
- * The container view displays the accessibility features.
+ * The menu view displays the accessibility features.
  */
 @SuppressLint("ViewConstructor")
-class MenuView extends FrameLayout {
+class MenuView extends FrameLayout implements
+        ViewTreeObserver.OnComputeInternalInsetsListener {
     private static final int INDEX_MENU_ITEM = 0;
     private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>();
     private final AccessibilityTargetAdapter mAdapter;
     private final MenuViewModel mMenuViewModel;
+    private final MenuAnimationController mMenuAnimationController;
+    private final Rect mBoundsInParent = new Rect();
     private final RecyclerView mTargetFeaturesView;
+    private final ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
+            this::updateSystemGestureExcludeRects;
+    private final Observer<MenuFadeEffectInfo> mFadeEffectInfoObserver =
+            this::onMenuFadeEffectInfoChanged;
+    private final Observer<Position> mPercentagePositionObserver = this::onPercentagePosition;
     private final Observer<Integer> mSizeTypeObserver = this::onSizeTypeChanged;
     private final Observer<List<AccessibilityTarget>> mTargetFeaturesObserver =
             this::onTargetFeaturesChanged;
@@ -53,23 +69,47 @@
 
         mMenuViewModel = menuViewModel;
         mMenuViewAppearance = menuViewAppearance;
+        mMenuAnimationController = new MenuAnimationController(this);
+
         mAdapter = new AccessibilityTargetAdapter(mTargetFeatures);
         mTargetFeaturesView = new RecyclerView(context);
         mTargetFeaturesView.setAdapter(mAdapter);
         mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context));
+        mTargetFeaturesView.setAccessibilityDelegateCompat(
+                new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) {
+                    @NonNull
+                    @Override
+                    public AccessibilityDelegateCompat getItemDelegate() {
+                        return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this,
+                                mMenuAnimationController);
+                    }
+                });
         setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
         // Avoid drawing out of bounds of the parent view
         setClipToOutline(true);
+
         loadLayoutResources();
 
         addView(mTargetFeaturesView);
     }
 
     @Override
+    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+        inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+        inoutInfo.touchableRegion.set(mBoundsInParent);
+    }
+
+    @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
 
         loadLayoutResources();
+
+        mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
+    }
+
+    void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) {
+        mTargetFeaturesView.addOnItemTouchListener(listener);
     }
 
     @SuppressLint("NotifyDataSetChanged")
@@ -80,11 +120,25 @@
     }
 
     private void onSizeChanged() {
+        mBoundsInParent.set(mBoundsInParent.left, mBoundsInParent.top,
+                mBoundsInParent.left + mMenuViewAppearance.getMenuWidth(),
+                mBoundsInParent.top + mMenuViewAppearance.getMenuHeight());
+
         final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
         layoutParams.height = mMenuViewAppearance.getMenuHeight();
         setLayoutParams(layoutParams);
     }
 
+    void onEdgeChangedIfNeeded() {
+        final Rect draggableBounds = mMenuViewAppearance.getMenuDraggableBounds();
+        if (getTranslationX() != draggableBounds.left
+                && getTranslationX() != draggableBounds.right) {
+            return;
+        }
+
+        onEdgeChanged();
+    }
+
     private void onEdgeChanged() {
         final int[] insets = mMenuViewAppearance.getMenuInsets();
         getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
@@ -96,8 +150,22 @@
                 mMenuViewAppearance.getMenuStrokeColor());
     }
 
+    private void onPercentagePosition(Position percentagePosition) {
+        mMenuViewAppearance.setPercentagePosition(percentagePosition);
+
+        onPositionChanged();
+    }
+
+    void onPositionChanged() {
+        final PointF position = mMenuViewAppearance.getMenuPosition();
+        mMenuAnimationController.moveToPosition(position);
+        onBoundsInParentChanged((int) position.x, (int) position.y);
+    }
+
     @SuppressLint("NotifyDataSetChanged")
     private void onSizeTypeChanged(int newSizeType) {
+        mMenuAnimationController.fadeInNowIfEnabled();
+
         mMenuViewAppearance.setSizeType(newSizeType);
 
         mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding());
@@ -106,41 +174,117 @@
 
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
+
+        mMenuAnimationController.fadeOutIfEnabled();
     }
 
     private void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures) {
         // TODO(b/252756133): Should update specific item instead of the whole list
+        mMenuAnimationController.fadeInNowIfEnabled();
+
         mTargetFeatures.clear();
         mTargetFeatures.addAll(newTargetFeatures);
         mMenuViewAppearance.setTargetFeaturesSize(mTargetFeatures.size());
+        mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
         mAdapter.notifyDataSetChanged();
 
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
+
+        mMenuAnimationController.fadeOutIfEnabled();
+    }
+
+    private void onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) {
+        mMenuAnimationController.updateOpacityWith(fadeEffectInfo.isFadeEffectEnabled(),
+                fadeEffectInfo.getOpacity());
+    }
+
+    Rect getMenuDraggableBounds() {
+        return mMenuViewAppearance.getMenuDraggableBounds();
+    }
+
+    void persistPositionAndUpdateEdge(Position percentagePosition) {
+        mMenuViewModel.updateMenuSavingPosition(percentagePosition);
+        mMenuViewAppearance.setPercentagePosition(percentagePosition);
+
+        onEdgeChangedIfNeeded();
+    }
+
+    /**
+     * Uses the touch events from the parent view to identify if users clicked the extra
+     * space of the menu view. If yes, will use the percentage position and update the
+     * translations of the menu view to meet the effect of moving out from the edge. It’s only
+     * used when the menu view is hidden to the screen edge.
+     *
+     * @param x the current x of the touch event from the parent {@link MenuViewLayer} of the
+     * {@link MenuView}.
+     * @param y the current y of the touch event from the parent {@link MenuViewLayer} of the
+     * {@link MenuView}.
+     * @return true if consume the touch event, otherwise false.
+     */
+    boolean maybeMoveOutEdgeAndShow(int x, int y) {
+        // Utilizes the touch region of the parent view to implement that users could tap extra
+        // the space region to show the menu from the edge.
+        if (!mMenuAnimationController.isMovedToEdge() || !mBoundsInParent.contains(x, y)) {
+            return false;
+        }
+
+        mMenuAnimationController.fadeInNowIfEnabled();
+
+        mMenuAnimationController.moveOutEdgeAndShow();
+
+        mMenuAnimationController.fadeOutIfEnabled();
+        return true;
     }
 
     void show() {
+        mMenuViewModel.getPercentagePositionData().observeForever(mPercentagePositionObserver);
+        mMenuViewModel.getFadeEffectInfoData().observeForever(mFadeEffectInfoObserver);
         mMenuViewModel.getTargetFeaturesData().observeForever(mTargetFeaturesObserver);
         mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver);
         setVisibility(VISIBLE);
         mMenuViewModel.registerContentObservers();
+        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+        getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
     }
 
     void hide() {
         setVisibility(GONE);
+        mBoundsInParent.setEmpty();
+        mMenuViewModel.getPercentagePositionData().removeObserver(mPercentagePositionObserver);
+        mMenuViewModel.getFadeEffectInfoData().removeObserver(mFadeEffectInfoObserver);
         mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver);
         mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver);
         mMenuViewModel.unregisterContentObservers();
+        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+        getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater);
+    }
+
+    void onDraggingStart() {
+        final int[] insets = mMenuViewAppearance.getMenuMovingStateInsets();
+        getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
+                insets[3]);
+
+        final GradientDrawable gradientDrawable = getContainerViewGradient();
+        gradientDrawable.setCornerRadii(mMenuViewAppearance.getMenuMovingStateRadii());
+    }
+
+    void onBoundsInParentChanged(int newLeft, int newTop) {
+        mBoundsInParent.offsetTo(newLeft, newTop);
     }
 
     void loadLayoutResources() {
         mMenuViewAppearance.update();
 
+        mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription());
         setBackground(mMenuViewAppearance.getMenuBackground());
         setElevation(mMenuViewAppearance.getMenuElevation());
         onItemSizeChanged();
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
     }
 
     private InstantInsetLayerDrawable getContainerViewInsetLayer() {
@@ -150,4 +294,9 @@
     private GradientDrawable getContainerViewGradient() {
         return (GradientDrawable) getContainerViewInsetLayer().getDrawable(INDEX_MENU_ITEM);
     }
+
+    private void updateSystemGestureExcludeRects() {
+        final ViewGroup parentView = (ViewGroup) getParent();
+        parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent));
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
index b9b7732..034e96a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
@@ -16,12 +16,21 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static android.view.View.OVER_SCROLL_ALWAYS;
+import static android.view.View.OVER_SCROLL_NEVER;
+
 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
 
 import android.annotation.IntDef;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.annotation.DimenRes;
 
@@ -34,9 +43,13 @@
  * Provides the layout resources information of the {@link MenuView}.
  */
 class MenuViewAppearance {
+    private final WindowManager mWindowManager;
     private final Resources mRes;
+    private final Position mPercentagePosition = new Position(/* percentageX= */
+            0f, /* percentageY= */ 0f);
     private int mTargetFeaturesSize;
     private int mSizeType;
+    private int mMargin;
     private int mSmallPadding;
     private int mLargePadding;
     private int mSmallIconSize;
@@ -51,6 +64,7 @@
     private int mElevation;
     private float[] mRadii;
     private Drawable mBackgroundDrawable;
+    private String mContentDescription;
 
     @IntDef({
             SMALL,
@@ -62,13 +76,15 @@
         int LARGE = 1;
     }
 
-    MenuViewAppearance(Context context) {
+    MenuViewAppearance(Context context, WindowManager windowManager) {
+        mWindowManager = windowManager;
         mRes = context.getResources();
 
         update();
     }
 
     void update() {
+        mMargin = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
         mSmallPadding =
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_padding);
         mLargePadding =
@@ -81,7 +97,7 @@
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_single_radius);
         mSmallMultipleRadius = mRes.getDimensionPixelSize(
                 R.dimen.accessibility_floating_menu_small_multiple_radius);
-        mRadii = createRadii(getMenuRadius(mTargetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
         mLargeSingleRadius =
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_single_radius);
         mLargeMultipleRadius = mRes.getDimensionPixelSize(
@@ -93,18 +109,59 @@
         final Drawable drawable =
                 mRes.getDrawable(R.drawable.accessibility_floating_menu_background);
         mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable});
+        mContentDescription = mRes.getString(
+                com.android.internal.R.string.accessibility_select_shortcut_menu_title);
     }
 
     void setSizeType(int sizeType) {
         mSizeType = sizeType;
 
-        mRadii = createRadii(getMenuRadius(mTargetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
     }
 
     void setTargetFeaturesSize(int targetFeaturesSize) {
         mTargetFeaturesSize = targetFeaturesSize;
 
-        mRadii = createRadii(getMenuRadius(targetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(targetFeaturesSize));
+    }
+
+    void setPercentagePosition(Position percentagePosition) {
+        mPercentagePosition.update(percentagePosition);
+
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
+    }
+
+    Rect getMenuDraggableBounds() {
+        final int margin = getMenuMargin();
+        final Rect draggableBounds = getWindowAvailableBounds();
+
+        // Initializes start position for mapping the translation of the menu view.
+        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+        final WindowInsets windowInsets = windowMetrics.getWindowInsets();
+        final Insets displayCutoutInsets = windowInsets.getInsetsIgnoringVisibility(
+                WindowInsets.Type.displayCutout());
+        draggableBounds.offset(-displayCutoutInsets.left, -displayCutoutInsets.top);
+
+        draggableBounds.top += margin;
+        draggableBounds.right -= getMenuWidth();
+        draggableBounds.bottom -= Math.min(
+                getWindowAvailableBounds().height() - draggableBounds.top,
+                calculateActualMenuHeight() + margin);
+        return draggableBounds;
+    }
+
+    PointF getMenuPosition() {
+        final Rect draggableBounds = getMenuDraggableBounds();
+
+        return new PointF(
+                draggableBounds.left
+                        + draggableBounds.width() * mPercentagePosition.getPercentageX(),
+                draggableBounds.top
+                        + draggableBounds.height() * mPercentagePosition.getPercentageY());
+    }
+
+    String getContentDescription() {
+        return mContentDescription;
     }
 
     Drawable getMenuBackground() {
@@ -115,20 +172,41 @@
         return mElevation;
     }
 
+    int getMenuWidth() {
+        return getMenuPadding() * 2 + getMenuIconSize();
+    }
+
     int getMenuHeight() {
-        return calculateActualMenuHeight();
+        return Math.min(getWindowAvailableBounds().height() - mMargin * 2,
+                calculateActualMenuHeight());
     }
 
     int getMenuIconSize() {
         return mSizeType == SMALL ? mSmallIconSize : mLargeIconSize;
     }
 
+    private int getMenuMargin() {
+        return mMargin;
+    }
+
     int getMenuPadding() {
         return mSizeType == SMALL ? mSmallPadding : mLargePadding;
     }
 
     int[] getMenuInsets() {
-        return new int[]{mInset, 0, 0, 0};
+        final int left = isMenuOnLeftSide() ? mInset : 0;
+        final int right = isMenuOnLeftSide() ? 0 : mInset;
+
+        return new int[]{left, 0, right, 0};
+    }
+
+    int[] getMenuMovingStateInsets() {
+        return new int[]{0, 0, 0, 0};
+    }
+
+    float[] getMenuMovingStateRadii() {
+        final float radius = getMenuRadius(mTargetFeaturesSize);
+        return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
     }
 
     int getMenuStrokeWidth() {
@@ -147,6 +225,14 @@
         return mSizeType == SMALL ? getSmallSize(itemCount) : getLargeSize(itemCount);
     }
 
+    int getMenuScrollMode() {
+        return hasExceededMaxWindowHeight() ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER;
+    }
+
+    private boolean hasExceededMaxWindowHeight() {
+        return calculateActualMenuHeight() > getWindowAvailableBounds().height();
+    }
+
     @DimenRes
     private int getSmallSize(int itemCount) {
         return itemCount > 1 ? mSmallMultipleRadius : mSmallSingleRadius;
@@ -157,8 +243,29 @@
         return itemCount > 1 ? mLargeMultipleRadius : mLargeSingleRadius;
     }
 
-    private static float[] createRadii(float radius) {
-        return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f};
+    private static float[] createRadii(boolean isMenuOnLeftSide, float radius) {
+        return isMenuOnLeftSide
+                ? new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f}
+                : new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
+    }
+
+    private Rect getWindowAvailableBounds() {
+        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+        final WindowInsets windowInsets = windowMetrics.getWindowInsets();
+        final Insets insets = windowInsets.getInsetsIgnoringVisibility(
+                WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
+
+        final Rect bounds = new Rect(windowMetrics.getBounds());
+        bounds.left += insets.left;
+        bounds.right -= insets.right;
+        bounds.top += insets.top;
+        bounds.bottom -= insets.bottom;
+
+        return bounds;
+    }
+
+    private boolean isMenuOnLeftSide() {
+        return mPercentagePosition.getPercentageX() < 0.5f;
     }
 
     private int calculateActualMenuHeight() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 4ea2f77..5252519 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -19,6 +19,8 @@
 import android.annotation.IntDef;
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.view.MotionEvent;
+import android.view.WindowManager;
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
@@ -41,17 +43,27 @@
         int MENU_VIEW = 0;
     }
 
-    MenuViewLayer(@NonNull Context context) {
+    MenuViewLayer(@NonNull Context context, WindowManager windowManager) {
         super(context);
 
         final MenuViewModel menuViewModel = new MenuViewModel(context);
-        final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context);
+        final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context,
+                windowManager);
         mMenuView = new MenuView(context, menuViewModel, menuViewAppearance);
 
         addView(mMenuView, LayerIndex.MENU_VIEW);
     }
 
     @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mMenuView.maybeMoveOutEdgeAndShow((int) event.getX(), (int) event.getY())) {
+            return true;
+        }
+
+        return super.onInterceptTouchEvent(event);
+    }
+
+    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
index 1e15a59..d2093c2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.graphics.PixelFormat;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 
 /**
@@ -33,7 +34,7 @@
 
     MenuViewLayerController(Context context, WindowManager windowManager) {
         mWindowManager = windowManager;
-        mMenuViewLayer = new MenuViewLayer(context);
+        mMenuViewLayer = new MenuViewLayer(context, windowManager);
     }
 
     @Override
@@ -68,9 +69,10 @@
                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                 PixelFormat.TRANSLUCENT);
+        params.receiveInsetsIgnoringZOrder = true;
         params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
         params.windowAnimations = android.R.style.Animation_Translucent;
-
+        params.setFitInsetsTypes(WindowInsets.Type.navigationBars());
         return params;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
index c3ba439..e8a2b6e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
@@ -33,6 +33,9 @@
     private final MutableLiveData<List<AccessibilityTarget>> mTargetFeaturesData =
             new MutableLiveData<>();
     private final MutableLiveData<Integer> mSizeTypeData = new MutableLiveData<>();
+    private final MutableLiveData<MenuFadeEffectInfo> mFadeEffectInfoData =
+            new MutableLiveData<>();
+    private final MutableLiveData<Position> mPercentagePositionData = new MutableLiveData<>();
     private final MenuInfoRepository mInfoRepository;
 
     MenuViewModel(Context context) {
@@ -49,11 +52,30 @@
         mSizeTypeData.setValue(newSizeType);
     }
 
+    @Override
+    public void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) {
+        mFadeEffectInfoData.setValue(fadeEffectInfo);
+    }
+
+    void updateMenuSavingPosition(Position percentagePosition) {
+        mInfoRepository.updateMenuSavingPosition(percentagePosition);
+    }
+
+    LiveData<Position> getPercentagePositionData() {
+        mInfoRepository.loadMenuPosition(mPercentagePositionData::setValue);
+        return mPercentagePositionData;
+    }
+
     LiveData<Integer> getSizeTypeData() {
         mInfoRepository.loadMenuSizeType(mSizeTypeData::setValue);
         return mSizeTypeData;
     }
 
+    LiveData<MenuFadeEffectInfo> getFadeEffectInfoData() {
+        mInfoRepository.loadMenuFadeEffectInfo(mFadeEffectInfoData::setValue);
+        return mFadeEffectInfoData;
+    }
+
     LiveData<List<AccessibilityTarget>> getTargetFeaturesData() {
         mInfoRepository.loadMenuTargetFeatures(mTargetFeaturesData::setValue);
         return mTargetFeaturesData;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
index 7b7eda8..fc21be2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility.floatingmenu;
 
 import android.annotation.FloatRange;
+import android.annotation.NonNull;
 import android.text.TextUtils;
 
 /**
@@ -62,6 +63,13 @@
     }
 
     /**
+     * Updates the position with {@code percentagePosition}.
+     */
+    public void update(@NonNull Position percentagePosition) {
+        update(percentagePosition.getPercentageX(), percentagePosition.getPercentageY());
+    }
+
+    /**
      * Updates the position with {@code percentageX} and {@code percentageY}.
      *
      * @param percentageX the new percentage of X-axis of the screen, from 0.0 to 1.0.
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 242a598..8c7e0ef 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -652,17 +652,6 @@
         mUdfpsController.onAodInterrupt(screenX, screenY, major, minor);
     }
 
-    /**
-     * Cancel a fingerprint scan manually. This will get rid of the white circle on the udfps
-     * sensor area even if the user hasn't explicitly lifted their finger yet.
-     */
-    public void onCancelUdfps() {
-        if (mUdfpsController == null) {
-            return;
-        }
-        mUdfpsController.onCancelUdfps();
-    }
-
     private void sendResultAndCleanUp(@DismissedReason int reason,
             @Nullable byte[] credentialAttestation) {
         if (mReceiver == null) {
@@ -792,7 +781,11 @@
             mUdfpsBounds = udfpsProp.getLocation().getRect();
             mUdfpsBounds.scale(mScaleFactor);
             mUdfpsController.updateOverlayParams(udfpsProp.sensorId,
-                    new UdfpsOverlayParams(mUdfpsBounds, mCachedDisplayInfo.getNaturalWidth(),
+                    new UdfpsOverlayParams(mUdfpsBounds, new Rect(
+                            0, mCachedDisplayInfo.getNaturalHeight() / 2,
+                            mCachedDisplayInfo.getNaturalWidth(),
+                            mCachedDisplayInfo.getNaturalHeight()),
+                            mCachedDisplayInfo.getNaturalWidth(),
                             mCachedDisplayInfo.getNaturalHeight(), mScaleFactor,
                             mCachedDisplayInfo.rotation));
             if (!Objects.equals(previousUdfpsBounds, mUdfpsBounds)) {
@@ -1017,8 +1010,6 @@
         } else {
             Log.w(TAG, "onBiometricError callback but dialog is gone");
         }
-
-        onCancelUdfps();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
index 5ed8986..76cd3f4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
@@ -24,6 +24,7 @@
 import android.graphics.Insets;
 import android.os.UserHandle;
 import android.text.InputType;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 import android.view.View;
@@ -151,39 +152,52 @@
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
 
-        if (mAuthCredentialInput == null || mAuthCredentialHeader == null
-                || mSubtitleView == null || mPasswordField == null || mErrorView == null) {
+        if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null
+                || mDescriptionView == null || mPasswordField == null || mErrorView == null) {
             return;
         }
 
-        // b/157910732 In AuthContainerView#getLayoutParams() we used to prevent jank risk when
-        // resizing by IME show or hide, we used to setFitInsetsTypes `~WindowInsets.Type.ime()` to
-        // LP. As a result this view needs to listen onApplyWindowInsets() and handle onLayout.
         int inputLeftBound;
         int inputTopBound;
         int headerRightBound = right;
+        int headerTopBounds = top;
+        final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom()
+                : mSubtitleView.getBottom();
+        final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom
+                : mDescriptionView.getBottom();
         if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-            inputTopBound = (bottom - (mPasswordField.getHeight() + mErrorView.getHeight())) / 2;
+            inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2;
             inputLeftBound = (right - left) / 2;
             headerRightBound = inputLeftBound;
+            headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset);
         } else {
-            inputTopBound = mSubtitleView.getBottom() + (bottom - mSubtitleView.getBottom()) / 2;
+            inputTopBound =
+                    descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2;
             inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2;
         }
 
-        mAuthCredentialHeader.layout(left, top, headerRightBound, bottom);
+        if (mDescriptionView.getBottom() > mBottomInset) {
+            mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom);
+        }
         mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom);
     }
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        final int newWidth = MeasureSpec.getSize(widthMeasureSpec);
         final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset;
 
-        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), newHeight);
+        setMeasuredDimension(newWidth, newHeight);
 
-        measureChildren(widthMeasureSpec,
-                MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.AT_MOST));
+        final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2,
+                MeasureSpec.AT_MOST);
+        final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED);
+        if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
+            measureChildren(halfWidthSpec, fullHeightSpec);
+        } else {
+            measureChildren(widthMeasureSpec, fullHeightSpec);
+        }
     }
 
     @NonNull
@@ -193,6 +207,20 @@
         final Insets bottomInset = insets.getInsets(ime());
         if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) {
             mBottomInset = bottomInset.bottom;
+            if (mBottomInset > 0
+                    && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
+                mTitleView.setSingleLine(true);
+                mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+                mTitleView.setMarqueeRepeatLimit(-1);
+                // select to enable marquee unless a screen reader is enabled
+                mTitleView.setSelected(!mAccessibilityManager.isEnabled()
+                        || !mAccessibilityManager.isTouchExplorationEnabled());
+            } else {
+                mTitleView.setSingleLine(false);
+                mTitleView.setEllipsize(null);
+                // select to enable marquee unless a screen reader is enabled
+                mTitleView.setSelected(false);
+            }
             requestLayout();
         }
         return insets;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
index 11498db..f9e44a0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
@@ -93,7 +93,9 @@
     @Override
     protected void onErrorTimeoutFinish() {
         super.onErrorTimeoutFinish();
-        mLockPatternView.setEnabled(true);
+        // select to enable marquee unless a screen reader is enabled
+        mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled()
+                || !mAccessibilityManager.isTouchExplorationEnabled());
     }
 
     public AuthCredentialPatternView(Context context, AttributeSet attrs) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
index d4176ac..fa623d1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
@@ -77,7 +77,7 @@
     protected final Handler mHandler;
     protected final LockPatternUtils mLockPatternUtils;
 
-    private final AccessibilityManager mAccessibilityManager;
+    protected final AccessibilityManager mAccessibilityManager;
     private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
 
@@ -86,10 +86,10 @@
     private boolean mShouldAnimatePanel;
     private boolean mShouldAnimateContents;
 
-    private TextView mTitleView;
+    protected TextView mTitleView;
     protected TextView mSubtitleView;
-    private TextView mDescriptionView;
-    private ImageView mIconView;
+    protected TextView mDescriptionView;
+    protected ImageView mIconView;
     protected TextView mErrorView;
 
     protected @Utils.CredentialType int mCredentialType;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 0f5a99c..3273d74 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -788,7 +788,7 @@
             // ACTION_UP/ACTION_CANCEL,  we need to be careful about not letting the screen
             // accidentally remain in high brightness mode. As a mitigation, queue a call to
             // cancel the fingerprint scan.
-            mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelUdfps,
+            mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::cancelAodInterrupt,
                     AOD_INTERRUPT_TIMEOUT_MILLIS);
             // using a hard-coded value for major and minor until it is available from the sensor
             onFingerDown(requestId, screenX, screenY, minor, major);
@@ -815,26 +815,22 @@
     }
 
     /**
-     * Cancel UDFPS affordances - ability to hide the UDFPS overlay before the user explicitly
-     * lifts their finger. Generally, this should be called on errors in the authentication flow.
-     *
-     * The sensor that triggers an AOD fingerprint interrupt (see onAodInterrupt) doesn't give
-     * ACTION_UP/ACTION_CANCEL events, so and AOD interrupt scan needs to be cancelled manually.
+     * The sensor that triggers {@link #onAodInterrupt} doesn't emit ACTION_UP or ACTION_CANCEL
+     * events, which means the fingerprint gesture created by the AOD interrupt needs to be
+     * cancelled manually.
      * This should be called when authentication either succeeds or fails. Failing to cancel the
      * scan will leave the display in the UDFPS mode until the user lifts their finger. On optical
      * sensors, this can result in illumination persisting for longer than necessary.
      */
-    void onCancelUdfps() {
+    @VisibleForTesting
+    void cancelAodInterrupt() {
         if (!mIsAodInterruptActive) {
             return;
         }
         if (mOverlay != null && mOverlay.getOverlayView() != null) {
             onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
         }
-        if (mCancelAodTimeoutAction != null) {
-            mCancelAodTimeoutAction.run();
-            mCancelAodTimeoutAction = null;
-        }
+        mCancelAodTimeoutAction = null;
         mIsAodInterruptActive = false;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 66a521c..7d01096 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -21,13 +21,18 @@
 import android.content.Context
 import android.graphics.PixelFormat
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
 import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
+import android.os.Build
 import android.os.RemoteException
+import android.provider.Settings
 import android.util.Log
 import android.util.RotationUtils
 import android.view.LayoutInflater
@@ -38,6 +43,7 @@
 import android.view.accessibility.AccessibilityManager
 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
 import androidx.annotation.LayoutRes
+import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
@@ -54,13 +60,16 @@
 
 private const val TAG = "UdfpsControllerOverlay"
 
+@VisibleForTesting
+const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
+
 /**
  * Keeps track of the overlay state and UI resources associated with a single FingerprintService
  * request. This state can persist across configuration changes via the [show] and [hide]
  * methods.
  */
 @UiThread
-class UdfpsControllerOverlay(
+class UdfpsControllerOverlay @JvmOverloads constructor(
     private val context: Context,
     fingerprintManager: FingerprintManager,
     private val inflater: LayoutInflater,
@@ -82,7 +91,8 @@
     @ShowReason val requestReason: Int,
     private val controllerCallback: IUdfpsOverlayControllerCallback,
     private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
-    private val activityLaunchAnimator: ActivityLaunchAnimator
+    private val activityLaunchAnimator: ActivityLaunchAnimator,
+    private val isDebuggable: Boolean = Build.IS_DEBUGGABLE
 ) {
     /** The view, when [isShowing], or null. */
     var overlayView: UdfpsView? = null
@@ -102,18 +112,19 @@
         gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
         flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
-          WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
+                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
         privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
         // Avoid announcing window title.
         accessibilityTitle = " "
     }
 
     /** A helper if the [requestReason] was due to enrollment. */
-    val enrollHelper: UdfpsEnrollHelper? = if (requestReason.isEnrollmentReason()) {
-        UdfpsEnrollHelper(context, fingerprintManager, requestReason)
-    } else {
-        null
-    }
+    val enrollHelper: UdfpsEnrollHelper? =
+        if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) {
+            UdfpsEnrollHelper(context, fingerprintManager, requestReason)
+        } else {
+            null
+        }
 
     /** If the overlay is currently showing. */
     val isShowing: Boolean
@@ -129,6 +140,17 @@
 
     private var touchExplorationEnabled = false
 
+    private fun shouldRemoveEnrollmentUi(): Boolean {
+        if (isDebuggable) {
+            return Settings.Global.getInt(
+                context.contentResolver,
+                SETTING_REMOVE_ENROLLMENT_UI,
+                0 /* def */
+            ) != 0
+        }
+        return false
+    }
+
     /** Show the overlay or return false and do nothing if it is already showing. */
     @SuppressLint("ClickableViewAccessibility")
     fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
@@ -183,7 +205,18 @@
         view: UdfpsView,
         controller: UdfpsController
     ): UdfpsAnimationViewController<*>? {
-        return when (requestReason) {
+        val isEnrollment = when (requestReason) {
+            REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
+            else -> false
+        }
+
+        val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) {
+            REASON_AUTH_OTHER
+        } else {
+            requestReason
+        }
+
+        return when (filteredRequestReason) {
             REASON_ENROLL_FIND_SENSOR,
             REASON_ENROLL_ENROLLING -> {
                 UdfpsEnrollViewController(
@@ -198,7 +231,7 @@
                     overlayParams.scaleFactor
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> {
+            REASON_AUTH_KEYGUARD -> {
                 UdfpsKeyguardViewController(
                     view.addUdfpsView(R.layout.udfps_keyguard_view),
                     statusBarStateController,
@@ -216,7 +249,7 @@
                     activityLaunchAnimator
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_BP -> {
+            REASON_AUTH_BP -> {
                 // note: empty controller, currently shows no visual affordance
                 UdfpsBpViewController(
                     view.addUdfpsView(R.layout.udfps_bp_view),
@@ -226,8 +259,8 @@
                     dumpManager
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_OTHER,
-            BiometricOverlayConstants.REASON_AUTH_SETTINGS -> {
+            REASON_AUTH_OTHER,
+            REASON_AUTH_SETTINGS -> {
                 UdfpsFpmOtherViewController(
                     view.addUdfpsView(R.layout.udfps_fpm_other_view),
                     statusBarStateController,
@@ -440,4 +473,4 @@
 private fun Int.isImportantForAccessibility() =
     this == REASON_ENROLL_FIND_SENSOR ||
             this == REASON_ENROLL_ENROLLING ||
-            this == BiometricOverlayConstants.REASON_AUTH_BP
+            this == REASON_AUTH_BP
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
index 39199d1..0d08b43 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
@@ -16,12 +16,12 @@
 
 package com.android.systemui.biometrics
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.UdfpsLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
new file mode 100644
index 0000000..6e78f3d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
@@ -0,0 +1,206 @@
+/*
+ * 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.biometrics
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
+import android.os.Handler
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.Execution
+import java.util.*
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private const val TAG = "UdfpsOverlay"
+
+@SuppressLint("ClickableViewAccessibility")
+@SysUISingleton
+class UdfpsOverlay
+@Inject
+constructor(
+    private val context: Context,
+    private val execution: Execution,
+    private val windowManager: WindowManager,
+    private val fingerprintManager: FingerprintManager?,
+    private val handler: Handler,
+    private val biometricExecutor: Executor,
+    private val alternateTouchProvider: Optional<AlternateUdfpsTouchProvider>,
+    private val fgExecutor: DelayableExecutor,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val authController: AuthController,
+    private val udfpsLogger: UdfpsLogger
+) : CoreStartable {
+
+    /** The view, when [isShowing], or null. */
+    var overlayView: UdfpsOverlayView? = null
+        private set
+
+    private var requestId: Long = 0
+    private var onFingerDown = false
+    val size = windowManager.maximumWindowMetrics.bounds
+    val udfpsProps: MutableList<FingerprintSensorPropertiesInternal> = mutableListOf()
+
+    private var params: UdfpsOverlayParams = UdfpsOverlayParams()
+
+    private val coreLayoutParams =
+        WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
+                0 /* flags set in computeLayoutParams() */,
+                PixelFormat.TRANSLUCENT
+            )
+            .apply {
+                title = TAG
+                fitInsetsTypes = 0
+                gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
+                layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                flags = Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS
+                privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+                // Avoid announcing window title.
+                accessibilityTitle = " "
+                inputFeatures = INPUT_FEATURE_SPY
+            }
+
+    fun onTouch(v: View, event: MotionEvent): Boolean {
+        val view = v as UdfpsOverlayView
+
+        return when (event.action) {
+            MotionEvent.ACTION_DOWN,
+            MotionEvent.ACTION_MOVE -> {
+                onFingerDown = true
+                if (!view.isDisplayConfigured && alternateTouchProvider.isPresent) {
+                    biometricExecutor.execute {
+                        alternateTouchProvider
+                            .get()
+                            .onPointerDown(
+                                requestId,
+                                event.x.toInt(),
+                                event.y.toInt(),
+                                event.touchMinor,
+                                event.touchMajor
+                            )
+                    }
+                    fgExecutor.execute {
+                        if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
+                            keyguardUpdateMonitor.onUdfpsPointerDown(requestId.toInt())
+                        }
+                    }
+
+                    view.configureDisplay {
+                        biometricExecutor.execute { alternateTouchProvider.get().onUiReady() }
+                    }
+                }
+
+                true
+            }
+            MotionEvent.ACTION_UP,
+            MotionEvent.ACTION_CANCEL -> {
+                if (onFingerDown && alternateTouchProvider.isPresent) {
+                    biometricExecutor.execute {
+                        alternateTouchProvider.get().onPointerUp(requestId)
+                    }
+                    fgExecutor.execute {
+                        if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
+                            keyguardUpdateMonitor.onUdfpsPointerUp(requestId.toInt())
+                        }
+                    }
+                }
+                onFingerDown = false
+                if (view.isDisplayConfigured) {
+                    view.unconfigureDisplay()
+                }
+
+                true
+            }
+            else -> false
+        }
+    }
+
+    fun show(requestId: Long): Boolean {
+        this.requestId = requestId
+        if (overlayView == null && alternateTouchProvider.isPresent) {
+            UdfpsOverlayView(context, null).let {
+                it.overlayParams = params
+                it.setUdfpsDisplayMode(
+                    UdfpsDisplayMode(context, execution, authController, udfpsLogger)
+                )
+                it.setOnTouchListener { v, event -> onTouch(v, event) }
+                overlayView = it
+            }
+            windowManager.addView(overlayView, coreLayoutParams)
+            return true
+        }
+
+        return false
+    }
+
+    fun hide() {
+        overlayView?.apply {
+            windowManager.removeView(this)
+            setOnTouchListener(null)
+        }
+
+        overlayView = null
+    }
+
+    @Override
+    override fun start() {
+        fingerprintManager?.addAuthenticatorsRegisteredCallback(
+            object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+                override fun onAllAuthenticatorsRegistered(
+                    sensors: List<FingerprintSensorPropertiesInternal>
+                ) {
+                    handler.post { handleAllFingerprintAuthenticatorsRegistered(sensors) }
+                }
+            }
+        )
+    }
+
+    private fun handleAllFingerprintAuthenticatorsRegistered(
+        sensors: List<FingerprintSensorPropertiesInternal>
+    ) {
+        for (props in sensors) {
+            if (props.isAnyUdfpsType) {
+                udfpsProps.add(props)
+            }
+        }
+
+        // Setup param size
+        if (udfpsProps.isNotEmpty()) {
+            params =
+                UdfpsOverlayParams(
+                    sensorBounds = udfpsProps[0].location.rect,
+                    overlayBounds = Rect(0, size.height() / 2, size.width(), size.height()),
+                    naturalDisplayWidth = size.width(),
+                    naturalDisplayHeight = size.height(),
+                    scaleFactor = 1f
+                )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
index d725dfb..c23b0f0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
@@ -20,6 +20,7 @@
 
 data class UdfpsOverlayParams(
     val sensorBounds: Rect = Rect(),
+    val overlayBounds: Rect = Rect(),
     val naturalDisplayWidth: Int = 0,
     val naturalDisplayHeight: Int = 0,
     val scaleFactor: Float = 1f,
@@ -40,4 +41,4 @@
         } else {
             naturalDisplayHeight
         }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
new file mode 100644
index 0000000..d371332
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.biometrics
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+private const val TAG = "UdfpsOverlayView"
+
+class UdfpsOverlayView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
+
+    private val sensorRect = RectF()
+    var overlayParams = UdfpsOverlayParams()
+    private var mUdfpsDisplayMode: UdfpsDisplayMode? = null
+
+    var overlayPaint = Paint()
+    var sensorPaint = Paint()
+    val centerPaint = Paint()
+
+    /** True after the call to [configureDisplay] and before the call to [unconfigureDisplay]. */
+    var isDisplayConfigured: Boolean = false
+        private set
+
+    init {
+        this.setWillNotDraw(false)
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        overlayPaint.color = Color.argb(120, 255, 0, 0)
+        overlayPaint.style = Paint.Style.FILL
+
+        sensorPaint.color = Color.argb(150, 134, 204, 255)
+        sensorPaint.style = Paint.Style.FILL
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        canvas.drawRect(overlayParams.overlayBounds, overlayPaint)
+        canvas.drawRect(overlayParams.sensorBounds, sensorPaint)
+        canvas.drawCircle(
+            overlayParams.sensorBounds.exactCenterX(),
+            overlayParams.sensorBounds.exactCenterY(),
+            overlayParams.sensorBounds.width().toFloat() / 2,
+            centerPaint
+        )
+    }
+
+    fun setUdfpsDisplayMode(udfpsDisplayMode: UdfpsDisplayMode?) {
+        mUdfpsDisplayMode = udfpsDisplayMode
+    }
+
+    fun configureDisplay(onDisplayConfigured: Runnable) {
+        isDisplayConfigured = true
+        mUdfpsDisplayMode?.enable(onDisplayConfigured)
+    }
+
+    fun unconfigureDisplay() {
+        isDisplayConfigured = false
+        mUdfpsDisplayMode?.disable(null /* onDisabled */)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
index b1d6e00..75640b7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.biometrics
 
+import android.content.Context
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
@@ -23,9 +24,9 @@
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
-import android.hardware.fingerprint.IUdfpsOverlayController
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
 import android.util.Log
+import android.view.LayoutInflater
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -41,14 +42,17 @@
  */
 @SysUISingleton
 class UdfpsShell @Inject constructor(
-    commandRegistry: CommandRegistry
+    commandRegistry: CommandRegistry,
+    private val udfpsOverlay: UdfpsOverlay
 ) : Command {
 
     /**
      * Set in [UdfpsController.java] constructor, used to show and hide the UDFPS overlay.
      * TODO: inject after b/229290039 is resolved
      */
-    var udfpsOverlayController: IUdfpsOverlayController? = null
+    var udfpsOverlayController: UdfpsController.UdfpsOverlayController? = null
+    var context: Context? = null
+    var inflater: LayoutInflater? = null
 
     init {
         commandRegistry.registerCommand("udfps") { this }
@@ -57,6 +61,11 @@
     override fun execute(pw: PrintWriter, args: List<String>) {
         if (args.size == 1 && args[0] == "hide") {
             hideOverlay()
+        } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "show") {
+            hideOverlay()
+            showUdfpsOverlay()
+        } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "hide") {
+            hideUdfpsOverlay()
         } else if (args.size == 2 && args[0] == "show") {
             showOverlay(getEnrollmentReason(args[1]))
         } else {
@@ -104,7 +113,17 @@
         )
     }
 
+    private fun showUdfpsOverlay() {
+        Log.v(TAG, "showUdfpsOverlay")
+        udfpsOverlay.show(REQUEST_ID)
+    }
+
+    private fun hideUdfpsOverlay() {
+        Log.v(TAG, "hideUdfpsOverlay")
+        udfpsOverlay.hide()
+    }
+
     private fun hideOverlay() {
         udfpsOverlayController?.hideUdfpsOverlay(SENSOR_ID)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
index 96af42b..d99625a 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.bluetooth
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.BluetoothLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Helper class for logging bluetooth events. */
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
index 5b3a982..d27708f 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
@@ -20,11 +20,11 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogMessage
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.log.dagger.BroadcastDispatcherLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
index 3871248..858bac3 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
@@ -44,9 +44,6 @@
     void onQsDown();
 
     /** */
-    void setQsExpanded(boolean expanded);
-
-    /** */
     boolean shouldEnforceBouncer();
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
index 28aac05..0b7d6ab 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
@@ -49,10 +49,6 @@
     }
 
     @Override
-    public void setQsExpanded(boolean expanded) {
-    }
-
-    @Override
     public boolean shouldEnforceBouncer() {
         return false;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index f5f9655..da3d293 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -23,6 +23,8 @@
 import android.util.Log;
 import android.view.MotionEvent;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.dagger.SysUISingleton;
@@ -30,6 +32,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
@@ -133,6 +136,7 @@
             ProximitySensor proximitySensor,
             StatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             BatteryController batteryController,
             DockManager dockManager,
             @Main DelayableExecutor mainExecutor,
@@ -157,6 +161,8 @@
 
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
 
+        shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
+
         mBatteryController.addCallback(mBatteryListener);
         mDockManager.addListener(mDockEventListener);
     }
@@ -193,8 +199,8 @@
     public void onQsDown() {
     }
 
-    @Override
-    public void setQsExpanded(boolean expanded) {
+    @VisibleForTesting
+    void onQsExpansionChanged(Boolean expanded) {
         if (expanded) {
             unregisterSensors();
         } else if (mSessionStarted) {
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
index bebade0..08e8293 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.common.shared.model
 
 import android.annotation.StringRes
+import android.content.Context
 
 /**
  * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or
@@ -30,4 +31,20 @@
     data class Resource(
         @StringRes val res: Int,
     ) : ContentDescription()
+
+    companion object {
+        /**
+         * Returns the loaded content description string, or null if we don't have one.
+         *
+         * Prefer [com.android.systemui.common.ui.binder.ContentDescriptionViewBinder.bind] over
+         * this method. This should only be used for testing or concatenation purposes.
+         */
+        fun ContentDescription?.loadContentDescription(context: Context): String? {
+            return when (this) {
+                null -> null
+                is Loaded -> this.description
+                is Resource -> context.getString(this.res)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
index 5d0e08f..4a56932 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.common.shared.model
 
 import android.annotation.StringRes
+import android.content.Context
 
 /**
  * Models a text, that can either be already [loaded][Text.Loaded] or be a [reference]
@@ -31,4 +32,20 @@
     data class Resource(
         @StringRes val res: Int,
     ) : Text()
+
+    companion object {
+        /**
+         * Returns the loaded test string, or null if we don't have one.
+         *
+         * Prefer [com.android.systemui.common.ui.binder.TextViewBinder.bind] over this method. This
+         * should only be used for testing or concatenation purposes.
+         */
+        fun Text?.loadText(context: Context): String? {
+            return when (this) {
+                null -> null
+                is Loaded -> this.text
+                is Resource -> context.getString(this.res)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
index 77b6523..d3b5d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
@@ -21,6 +21,8 @@
 import android.content.Intent
 import android.content.IntentFilter
 import android.os.Bundle
+import android.os.RemoteException
+import android.service.dreams.IDreamManager
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowInsets
@@ -40,11 +42,13 @@
  */
 class ControlsActivity @Inject constructor(
     private val uiController: ControlsUiController,
-    private val broadcastDispatcher: BroadcastDispatcher
+    private val broadcastDispatcher: BroadcastDispatcher,
+    private val dreamManager: IDreamManager,
 ) : ComponentActivity() {
 
     private lateinit var parent: ViewGroup
     private lateinit var broadcastReceiver: BroadcastReceiver
+    private var mExitToDream: Boolean = false
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -81,17 +85,36 @@
 
         parent = requireViewById<ViewGroup>(R.id.global_actions_controls)
         parent.alpha = 0f
-        uiController.show(parent, { finish() }, this)
+        uiController.show(parent, { finishOrReturnToDream() }, this)
 
         ControlsAnimations.enterAnimation(parent).start()
     }
 
-    override fun onBackPressed() {
+    override fun onResume() {
+        super.onResume()
+        mExitToDream = intent.getBooleanExtra(ControlsUiController.EXIT_TO_DREAM, false)
+    }
+
+    fun finishOrReturnToDream() {
+        if (mExitToDream) {
+            try {
+                mExitToDream = false
+                dreamManager.dream()
+                return
+            } catch (e: RemoteException) {
+                // Fall through
+            }
+        }
         finish()
     }
 
+    override fun onBackPressed() {
+        finishOrReturnToDream()
+    }
+
     override fun onStop() {
         super.onStop()
+        mExitToDream = false
 
         uiController.hide()
     }
@@ -106,7 +129,8 @@
         broadcastReceiver = object : BroadcastReceiver() {
             override fun onReceive(context: Context, intent: Intent) {
                 val action = intent.getAction()
-                if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                if (action == Intent.ACTION_SCREEN_OFF ||
+                    action == Intent.ACTION_DREAMING_STARTED) {
                     finish()
                 }
             }
@@ -114,6 +138,7 @@
 
         val filter = IntentFilter()
         filter.addAction(Intent.ACTION_SCREEN_OFF)
+        filter.addAction(Intent.ACTION_DREAMING_STARTED)
         broadcastDispatcher.registerReceiver(broadcastReceiver, filter)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
index 822f8f2..c1cfbcb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -27,6 +27,7 @@
     companion object {
         public const val TAG = "ControlsUiController"
         public const val EXTRA_ANIMATE = "extra_animate"
+        public const val EXIT_TO_DREAM = "extra_exit_to_dream"
     }
 
     fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context)
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 721c0ba..09743ef 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.accessibility.SystemActions
 import com.android.systemui.accessibility.WindowMagnification
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.UdfpsOverlay
 import com.android.systemui.clipboardoverlay.ClipboardListener
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.globalactions.GlobalActionsComponent
@@ -218,6 +219,12 @@
     @ClassKey(KeyguardLiftController::class)
     abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable
 
+    /** Inject into UdfpsOverlay.  */
+    @Binds
+    @IntoMap
+    @ClassKey(UdfpsOverlay::class)
+    abstract fun bindUdfpsOverlay(sysui: UdfpsOverlay): CoreStartable
+
     /** Inject into MediaTttSenderCoordinator. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index d7638d6..7e31626 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -23,7 +23,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.keyguard.clock.ClockModule;
+import com.android.keyguard.clock.ClockInfoModule;
+import com.android.keyguard.dagger.ClockRegistryModule;
 import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.BootCompleteCache;
 import com.android.systemui.BootCompleteCacheImpl;
@@ -120,7 +121,8 @@
             BiometricsModule.class,
             BouncerViewModule.class,
             ClipboardOverlayModule.class,
-            ClockModule.class,
+            ClockInfoModule.class,
+            ClockRegistryModule.class,
             CoroutinesModule.class,
             DreamModule.class,
             ControlsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 5fdd198..c256e44 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -99,7 +99,7 @@
     }
 
     fun shouldShowFaceScanningAnim(): Boolean {
-        return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceScanning
+        return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceDetectionRunning
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index cc57662..f8e2566 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -19,10 +19,10 @@
 import android.view.Display
 import com.android.systemui.doze.DozeLog.Reason
 import com.android.systemui.doze.DozeLog.reasonToString
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.log.dagger.DozeLog
 import com.android.systemui.statusbar.policy.DevicePostureController
 import java.text.SimpleDateFormat
@@ -224,10 +224,14 @@
         })
     }
 
-    fun logPulseDropped(from: String, state: DozeMachine.State) {
+    /**
+     * Log why a pulse was dropped and the current doze machine state. The state can be null
+     * if the DozeMachine is the middle of transitioning between states.
+     */
+    fun logPulseDropped(from: String, state: DozeMachine.State?) {
         buffer.log(TAG, INFO, {
             str1 = from
-            str2 = state.name
+            str2 = state?.name
         }, {
             "Pulse dropped, cannot pulse from=$str1 state=$str2"
         })
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index ae41215..96c35d4 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -20,7 +20,6 @@
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
 
 import android.annotation.MainThread;
-import android.app.UiModeManager;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Trace;
@@ -145,10 +144,9 @@
 
     private final Service mDozeService;
     private final WakeLock mWakeLock;
-    private final AmbientDisplayConfiguration mConfig;
+    private final AmbientDisplayConfiguration mAmbientDisplayConfig;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final DozeHost mDozeHost;
-    private final UiModeManager mUiModeManager;
     private final DockManager mDockManager;
     private final Part[] mParts;
 
@@ -156,18 +154,18 @@
     private State mState = State.UNINITIALIZED;
     private int mPulseReason;
     private boolean mWakeLockHeldForCurrentState = false;
+    private int mUiModeType = Configuration.UI_MODE_TYPE_NORMAL;
 
     @Inject
-    public DozeMachine(@WrappedService Service service, AmbientDisplayConfiguration config,
+    public DozeMachine(@WrappedService Service service,
+            AmbientDisplayConfiguration ambientDisplayConfig,
             WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle,
-            UiModeManager uiModeManager,
             DozeLog dozeLog, DockManager dockManager,
             DozeHost dozeHost, Part[] parts) {
         mDozeService = service;
-        mConfig = config;
+        mAmbientDisplayConfig = ambientDisplayConfig;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mWakeLock = wakeLock;
-        mUiModeManager = uiModeManager;
         mDozeLog = dozeLog;
         mDockManager = dockManager;
         mDozeHost = dozeHost;
@@ -187,6 +185,18 @@
     }
 
     /**
+     * Notifies the {@link DozeMachine} that {@link Configuration} has changed.
+     */
+    public void onConfigurationChanged(Configuration newConfiguration) {
+        int newUiModeType = newConfiguration.uiMode & Configuration.UI_MODE_TYPE_MASK;
+        if (mUiModeType == newUiModeType) return;
+        mUiModeType = newUiModeType;
+        for (Part part : mParts) {
+            part.onUiModeTypeChanged(mUiModeType);
+        }
+    }
+
+    /**
      * Requests transitioning to {@code requestedState}.
      *
      * This can be called during a state transition, in which case it will be queued until all
@@ -211,6 +221,14 @@
         requestState(State.DOZE_REQUEST_PULSE, pulseReason);
     }
 
+    /**
+     * @return true if {@link DozeMachine} is currently in either {@link State#UNINITIALIZED}
+     *  or {@link State#FINISH}
+     */
+    public boolean isUninitializedOrFinished() {
+        return mState == State.UNINITIALIZED || mState == State.FINISH;
+    }
+
     void onScreenState(int state) {
         mDozeLog.traceDisplayState(state);
         for (Part part : mParts) {
@@ -360,7 +378,7 @@
         if (mState == State.FINISH) {
             return State.FINISH;
         }
-        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR
+        if (mUiModeType == Configuration.UI_MODE_TYPE_CAR
                 && (requestedState.canPulse() || requestedState.staysAwake())) {
             Log.i(TAG, "Doze is suppressed with all triggers disabled as car mode is active");
             mDozeLog.traceCarModeStarted();
@@ -411,7 +429,7 @@
                     nextState = State.FINISH;
                 } else if (mDockManager.isDocked()) {
                     nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED;
-                } else if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
+                } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
                     nextState = State.DOZE_AOD;
                 } else {
                     nextState = State.DOZE;
@@ -427,6 +445,7 @@
     /** Dumps the current state */
     public void dump(PrintWriter pw) {
         pw.print(" state="); pw.println(mState);
+        pw.print(" mUiModeType="); pw.println(mUiModeType);
         pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState);
         pw.print(" wakeLock="); pw.println(mWakeLock);
         pw.println("Parts:");
@@ -459,6 +478,19 @@
 
         /** Sets the {@link DozeMachine} when this Part is associated with one. */
         default void setDozeMachine(DozeMachine dozeMachine) {}
+
+        /**
+         * Notifies the Part about a change in {@link Configuration#uiMode}.
+         *
+         * @param newUiModeType {@link Configuration#UI_MODE_TYPE_NORMAL},
+         *                   {@link Configuration#UI_MODE_TYPE_DESK},
+         *                   {@link Configuration#UI_MODE_TYPE_CAR},
+         *                   {@link Configuration#UI_MODE_TYPE_TELEVISION},
+         *                   {@link Configuration#UI_MODE_TYPE_APPLIANCE},
+         *                   {@link Configuration#UI_MODE_TYPE_WATCH},
+         *                   or {@link Configuration#UI_MODE_TYPE_VR_HEADSET}
+         */
+        default void onUiModeTypeChanged(int newUiModeType) {}
     }
 
     /** A wrapper interface for {@link android.service.dreams.DreamService} */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index a2eb4e3..e8d7e46 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -17,6 +17,7 @@
 package com.android.systemui.doze;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.service.dreams.DreamService;
@@ -59,6 +60,7 @@
         mPluginManager.addPluginListener(this, DozeServicePlugin.class, false /* allowMultiple */);
         DozeComponent dozeComponent = mDozeComponentBuilder.build(this);
         mDozeMachine = dozeComponent.getDozeMachine();
+        mDozeMachine.onConfigurationChanged(getResources().getConfiguration());
     }
 
     @Override
@@ -127,6 +129,12 @@
     }
 
     @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDozeMachine.onConfigurationChanged(newConfig);
+    }
+
+    @Override
     public void onRequestHideDoze() {
         if (mDozeMachine != null) {
             mDozeMachine.requestState(DozeMachine.State.DOZE);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
index 7ed4b35..e6d9865 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
@@ -16,21 +16,13 @@
 
 package com.android.systemui.doze;
 
-import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
-import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
 
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.text.TextUtils;
 
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.doze.dagger.DozeScope;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 
@@ -43,7 +35,9 @@
 /**
  * Handles suppressing doze on:
  * 1. INITIALIZED, don't allow dozing at all when:
- *      - in CAR_MODE
+ *      - in CAR_MODE, in this scenario the device is asleep and won't listen for any triggers
+ *      to wake up. In this state, no UI shows. Unlike other conditions, this suppression is only
+ *      temporary and stops when the device exits CAR_MODE
  *      - device is NOT provisioned
  *      - there's a pending authentication
  * 2. PowerSaveMode active
@@ -57,35 +51,47 @@
  */
 @DozeScope
 public class DozeSuppressor implements DozeMachine.Part {
-    private static final String TAG = "DozeSuppressor";
 
     private DozeMachine mMachine;
     private final DozeHost mDozeHost;
     private final AmbientDisplayConfiguration mConfig;
     private final DozeLog mDozeLog;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final UiModeManager mUiModeManager;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
 
-    private boolean mBroadcastReceiverRegistered;
+    private boolean mIsCarModeEnabled = false;
 
     @Inject
     public DozeSuppressor(
             DozeHost dozeHost,
             AmbientDisplayConfiguration config,
             DozeLog dozeLog,
-            BroadcastDispatcher broadcastDispatcher,
-            UiModeManager uiModeManager,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy) {
         mDozeHost = dozeHost;
         mConfig = config;
         mDozeLog = dozeLog;
-        mBroadcastDispatcher = broadcastDispatcher;
-        mUiModeManager = uiModeManager;
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
     }
 
     @Override
+    public void onUiModeTypeChanged(int newUiModeType) {
+        boolean isCarModeEnabled = newUiModeType == UI_MODE_TYPE_CAR;
+        if (mIsCarModeEnabled == isCarModeEnabled) {
+            return;
+        }
+        mIsCarModeEnabled = isCarModeEnabled;
+        // Do not handle the event if doze machine is not initialized yet.
+        // It will be handled upon initialization.
+        if (mMachine.isUninitializedOrFinished()) {
+            return;
+        }
+        if (mIsCarModeEnabled) {
+            handleCarModeStarted();
+        } else {
+            handleCarModeExited();
+        }
+    }
+
+    @Override
     public void setDozeMachine(DozeMachine dozeMachine) {
         mMachine = dozeMachine;
     }
@@ -94,7 +100,6 @@
     public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
         switch (newState) {
             case INITIALIZED:
-                registerBroadcastReceiver();
                 mDozeHost.addCallback(mHostCallback);
                 checkShouldImmediatelyEndDoze();
                 checkShouldImmediatelySuspendDoze();
@@ -108,14 +113,12 @@
 
     @Override
     public void destroy() {
-        unregisterBroadcastReceiver();
         mDozeHost.removeCallback(mHostCallback);
     }
 
     private void checkShouldImmediatelySuspendDoze() {
-        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
-            mDozeLog.traceCarModeStarted();
-            mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
+        if (mIsCarModeEnabled) {
+            handleCarModeStarted();
         }
     }
 
@@ -135,7 +138,7 @@
 
     @Override
     public void dump(PrintWriter pw) {
-        pw.println(" uiMode=" + mUiModeManager.getCurrentModeType());
+        pw.println(" isCarModeEnabled=" + mIsCarModeEnabled);
         pw.println(" hasPendingAuth="
                 + mBiometricUnlockControllerLazy.get().hasPendingAuthentication());
         pw.println(" isProvisioned=" + mDozeHost.isProvisioned());
@@ -143,40 +146,18 @@
         pw.println(" aodPowerSaveActive=" + mDozeHost.isPowerSaveActive());
     }
 
-    private void registerBroadcastReceiver() {
-        if (mBroadcastReceiverRegistered) {
-            return;
-        }
-        IntentFilter filter = new IntentFilter(ACTION_ENTER_CAR_MODE);
-        filter.addAction(ACTION_EXIT_CAR_MODE);
-        mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter);
-        mBroadcastReceiverRegistered = true;
+    private void handleCarModeExited() {
+        mDozeLog.traceCarModeEnded();
+        mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
+                ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
     }
 
-    private void unregisterBroadcastReceiver() {
-        if (!mBroadcastReceiverRegistered) {
-            return;
-        }
-        mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
-        mBroadcastReceiverRegistered = false;
+    private void handleCarModeStarted() {
+        mDozeLog.traceCarModeStarted();
+        mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
     }
 
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (ACTION_ENTER_CAR_MODE.equals(action)) {
-                mDozeLog.traceCarModeStarted();
-                mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
-            } else if (ACTION_EXIT_CAR_MODE.equals(action)) {
-                mDozeLog.traceCarModeEnded();
-                mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
-                        ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
-            }
-        }
-    };
-
-    private DozeHost.Callback mHostCallback = new DozeHost.Callback() {
+    private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
         @Override
         public void onPowerSaveChanged(boolean active) {
             // handles suppression changes, while DozeMachine#transitionPolicy handles gating
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index ef454ff..97a2179 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -536,13 +536,13 @@
             return;
         }
 
-        if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse()) {
+        if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse(dozeState)) {
             if (!mAllowPulseTriggers) {
                 mDozeLog.tracePulseDropped("requestPulse - !mAllowPulseTriggers");
             } else if (mDozeHost.isPulsePending()) {
                 mDozeLog.tracePulseDropped("requestPulse - pulsePending");
-            } else if (!canPulse()) {
-                mDozeLog.tracePulseDropped("requestPulse", dozeState);
+            } else if (!canPulse(dozeState)) {
+                mDozeLog.tracePulseDropped("requestPulse - dozeState cannot pulse", dozeState);
             }
             runIfNotNull(onPulseSuppressedListener);
             return;
@@ -559,15 +559,16 @@
                 // not in pocket, continue pulsing
                 final boolean isPulsePending = mDozeHost.isPulsePending();
                 mDozeHost.setPulsePending(false);
-                if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse()) {
+                if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse(dozeState)) {
                     if (!isPulsePending) {
                         mDozeLog.tracePulseDropped("continuePulseRequest - pulse no longer"
                                 + " pending, pulse was cancelled before it could start"
                                 + " transitioning to pulsing state.");
                     } else if (mDozeHost.isPulsingBlocked()) {
                         mDozeLog.tracePulseDropped("continuePulseRequest - pulsingBlocked");
-                    } else if (!canPulse()) {
-                        mDozeLog.tracePulseDropped("continuePulseRequest", mMachine.getState());
+                    } else if (!canPulse(dozeState)) {
+                        mDozeLog.tracePulseDropped("continuePulseRequest"
+                                + " - doze state cannot pulse", dozeState);
                     }
                     runIfNotNull(onPulseSuppressedListener);
                     return;
@@ -582,10 +583,10 @@
                 .ifPresent(uiEventEnum -> mUiEventLogger.log(uiEventEnum, getKeyguardSessionId()));
     }
 
-    private boolean canPulse() {
-        return mMachine.getState() == DozeMachine.State.DOZE
-                || mMachine.getState() == DozeMachine.State.DOZE_AOD
-                || mMachine.getState() == DozeMachine.State.DOZE_AOD_DOCKED;
+    private boolean canPulse(DozeMachine.State dozeState) {
+        return dozeState == DozeMachine.State.DOZE
+                || dozeState == DozeMachine.State.DOZE_AOD
+                || dozeState == DozeMachine.State.DOZE_AOD_DOCKED;
     }
 
     @Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index 0ccb222..cedd850a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -210,7 +210,8 @@
 
             final Intent intent = new Intent(mContext, ControlsActivity.class)
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
-                    .putExtra(ControlsUiController.EXTRA_ANIMATE, true);
+                    .putExtra(ControlsUiController.EXTRA_ANIMATE, true)
+                    .putExtra(ControlsUiController.EXIT_TO_DREAM, true);
 
             final ActivityLaunchAnimator.Controller controller =
                     v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */)
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
index 08ef8f3..609bd76 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
@@ -24,8 +24,13 @@
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
+import com.google.protobuf.nano.MessageNano
+import java.io.BufferedOutputStream
+import java.io.FileDescriptor
+import java.io.FileOutputStream
 import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Provider
@@ -100,7 +105,7 @@
     /**
      * Dump the diagnostics! Behavior can be controlled via [args].
      */
-    fun dump(pw: PrintWriter, args: Array<String>) {
+    fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
         Trace.beginSection("DumpManager#dump()")
         val start = SystemClock.uptimeMillis()
 
@@ -111,10 +116,12 @@
             return
         }
 
-        when (parsedArgs.dumpPriority) {
-            PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
-            PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs)
-            else -> dumpParameterized(pw, parsedArgs)
+        when {
+            parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
+            parsedArgs.dumpPriority == PRIORITY_ARG_NORMAL && !parsedArgs.proto -> {
+                dumpNormal(pw, parsedArgs)
+            }
+            else -> dumpParameterized(fd, pw, parsedArgs)
         }
 
         pw.println()
@@ -122,7 +129,7 @@
         Trace.endSection()
     }
 
-    private fun dumpParameterized(pw: PrintWriter, args: ParsedArgs) {
+    private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
         when (args.command) {
             "bugreport-critical" -> dumpCritical(pw, args)
             "bugreport-normal" -> dumpNormal(pw, args)
@@ -130,7 +137,13 @@
             "buffers" -> dumpBuffers(pw, args)
             "config" -> dumpConfig(pw)
             "help" -> dumpHelp(pw)
-            else -> dumpTargets(args.nonFlagArgs, pw, args)
+            else -> {
+                if (args.proto) {
+                    dumpProtoTargets(args.nonFlagArgs, fd, args)
+                } else {
+                    dumpTargets(args.nonFlagArgs, pw, args)
+                }
+            }
         }
     }
 
@@ -160,6 +173,26 @@
         }
     }
 
+    private fun dumpProtoTargets(
+            targets: List<String>,
+            fd: FileDescriptor,
+            args: ParsedArgs
+    ) {
+        val systemUIProto = SystemUIProtoDump()
+        if (targets.isNotEmpty()) {
+            for (target in targets) {
+                dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs)
+            }
+        } else {
+            dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs)
+        }
+        val buffer = BufferedOutputStream(FileOutputStream(fd))
+        buffer.use {
+            it.write(MessageNano.toByteArray(systemUIProto))
+            it.flush()
+        }
+    }
+
     private fun dumpTargets(
         targets: List<String>,
         pw: PrintWriter,
@@ -235,6 +268,7 @@
         pw.println("$ <invocation> buffers")
         pw.println("$ <invocation> bugreport-critical")
         pw.println("$ <invocation> bugreport-normal")
+        pw.println("$ <invocation> config")
         pw.println()
 
         pw.println("Targets can be listed:")
@@ -266,6 +300,7 @@
                             }
                         }
                     }
+                    PROTO -> pArgs.proto = true
                     "-t", "--tail" -> {
                         pArgs.tailLength = readArgument(iterator, arg) {
                             it.toInt()
@@ -277,6 +312,9 @@
                     "-h", "--help" -> {
                         pArgs.command = "help"
                     }
+                    // This flag is passed as part of the proto dump in Bug reports, we can ignore
+                    // it because this is our default behavior.
+                    "-a" -> {}
                     else -> {
                         throw ArgParseException("Unknown flag: $arg")
                     }
@@ -313,13 +351,21 @@
         const val PRIORITY_ARG_CRITICAL = "CRITICAL"
         const val PRIORITY_ARG_HIGH = "HIGH"
         const val PRIORITY_ARG_NORMAL = "NORMAL"
+        const val PROTO = "--proto"
     }
 }
 
 private val PRIORITY_OPTIONS =
         arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL)
 
-private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables")
+private val COMMANDS = arrayOf(
+        "bugreport-critical",
+        "bugreport-normal",
+        "buffers",
+        "dumpables",
+        "config",
+        "help"
+)
 
 private class ParsedArgs(
     val rawArgs: Array<String>,
@@ -329,6 +375,7 @@
     var tailLength: Int = 0
     var command: String? = null
     var listOnly = false
+    var proto = false
 }
 
 class ArgParseException(message: String) : Exception(message)
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index cca04da..ae78089 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -18,7 +18,9 @@
 
 import android.util.ArrayMap
 import com.android.systemui.Dumpable
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.ProtoDumpable
+import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -90,7 +92,7 @@
         target: String,
         pw: PrintWriter,
         args: Array<String>,
-        tailLength: Int
+        tailLength: Int,
     ) {
         for (dumpable in dumpables.values) {
             if (dumpable.name.endsWith(target)) {
@@ -107,6 +109,36 @@
         }
     }
 
+    @Synchronized
+    fun dumpProtoTarget(
+        target: String,
+        protoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        for (dumpable in dumpables.values) {
+            if (dumpable.dumpable is ProtoDumpable && dumpable.name.endsWith(target)) {
+                dumpProtoDumpable(dumpable.dumpable, protoDump, args)
+                return
+            }
+        }
+    }
+
+    @Synchronized
+    fun dumpProtoDumpables(
+        systemUIProtoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        for (dumpable in dumpables.values) {
+            if (dumpable.dumpable is ProtoDumpable) {
+                dumpProtoDumpable(
+                    dumpable.dumpable,
+                    systemUIProtoDump,
+                    args
+                )
+            }
+        }
+    }
+
     /**
      * Dumps all registered dumpables to [pw]
      */
@@ -184,6 +216,14 @@
         buffer.dumpable.dump(pw, tailLength)
     }
 
+    private fun dumpProtoDumpable(
+        protoDumpable: ProtoDumpable,
+        systemUIProtoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        protoDumpable.dumpProto(systemUIProtoDump, args)
+    }
+
     private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean {
         val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable
         return existingDumpable == null || newDumpable == existingDumpable
@@ -195,4 +235,4 @@
     val dumpable: T
 )
 
-private const val TAG = "DumpManager"
\ No newline at end of file
+private const val TAG = "DumpManager"
diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
index 0eab1af..8299b13 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
@@ -19,7 +19,7 @@
 import android.content.Context
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.util.io.Files
 import com.android.systemui.util.time.SystemClock
 import java.io.IOException
diff --git a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
index 0a41a56..da983ab 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
+++ b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
@@ -51,6 +51,7 @@
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         // Simulate the NORMAL priority arg being passed to us
         mDumpHandler.dump(
+                fd,
                 pw,
                 new String[] { DumpHandler.PRIORITY_ARG, DumpHandler.PRIORITY_ARG_NORMAL });
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto
similarity index 69%
copy from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
copy to packages/SystemUI/src/com/android/systemui/dump/sysui.proto
index 23072a2..cd8c08a 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto
@@ -13,9 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+syntax = "proto3";
 
-package com.android.settingslib.spa.gallery
+package com.android.systemui.dump;
 
-import com.android.settingslib.spa.framework.DebugActivity
+import "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto";
 
-class GalleryDebugActivity : DebugActivity()
+option java_multiple_files = true;
+
+message SystemUIProtoDump {
+  repeated com.android.systemui.qs.QsTileState tiles = 1;
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
deleted file mode 100644
index ab1e8c6..0000000
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ /dev/null
@@ -1,364 +0,0 @@
-/*
- * Copyright (C) 2021 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.flags;
-
-import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER;
-
-import com.android.internal.annotations.Keep;
-import com.android.systemui.R;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * List of {@link Flag} objects for use in SystemUI.
- *
- * Flag Ids are integers.
- * Ids must be unique. This is enforced in a unit test.
- * Ids need not be sequential. Flags can "claim" a chunk of ids for flags in related features with
- * a comment. This is purely for organizational purposes.
- *
- * On public release builds, flags will always return their default value. There is no way to
- * change their value on release builds.
- *
- * See {@link FeatureFlagsDebug} for instructions on flipping the flags via adb.
- */
-public class Flags {
-    public static final UnreleasedFlag TEAMFOOD = new UnreleasedFlag(1);
-
-    /***************************************/
-    // 100 - notification
-    public static final UnreleasedFlag NOTIFICATION_PIPELINE_DEVELOPER_LOGGING =
-            new UnreleasedFlag(103);
-
-    public static final UnreleasedFlag NSSL_DEBUG_LINES =
-            new UnreleasedFlag(105);
-
-    public static final UnreleasedFlag NSSL_DEBUG_REMOVE_ANIMATION =
-            new UnreleasedFlag(106);
-
-    public static final UnreleasedFlag NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE =
-            new UnreleasedFlag(107);
-
-    public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS =
-            new ResourceBooleanFlag(108, R.bool.config_notificationToContents);
-
-    public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS =
-            new ReleasedFlag(109);
-
-    public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD =
-            new UnreleasedFlag(110, true);
-
-    public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
-
-    public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
-            false);
-
-    public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true);
-
-    // next id: 114
-
-    /***************************************/
-    // 200 - keyguard/lockscreen
-
-    // ** Flag retired **
-    // public static final BooleanFlag KEYGUARD_LAYOUT =
-    //         new BooleanFlag(200, true);
-
-    public static final ReleasedFlag LOCKSCREEN_ANIMATIONS =
-            new ReleasedFlag(201);
-
-    public static final ReleasedFlag NEW_UNLOCK_SWIPE_ANIMATION =
-            new ReleasedFlag(202);
-
-    public static final ResourceBooleanFlag CHARGING_RIPPLE =
-            new ResourceBooleanFlag(203, R.bool.flag_charging_ripple);
-
-    public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER =
-            new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher);
-
-    public static final ResourceBooleanFlag FACE_SCANNING_ANIM =
-            new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation);
-
-    public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207);
-  
-    /**
-     * Flag to enable the usage of the new bouncer data source. This is a refactor of and
-     * eventual replacement of KeyguardBouncer.java.
-     */
-    public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208);
-
-    /**
-     * Whether the user interactor and repository should use `UserSwitcherController`.
-     *
-     * <p>If this is {@code false}, the interactor and repo skip the controller and directly access
-     * the framework APIs.
-     */
-    public static final ReleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
-            new ReleasedFlag(210);
-
-    /**
-     * Whether `UserSwitcherController` should use the user interactor.
-     *
-     * <p>When this is {@code true}, the controller does not directly access framework APIs.
-     * Instead, it goes through the interactor.
-     *
-     * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
-     * {@code true} as it would created a cycle between controller -> interactor -> controller.
-     */
-    public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR = new UnreleasedFlag(211);
-
-    /**
-     * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
-     * the digits when the clock moves.
-     */
-    public static final UnreleasedFlag STEP_CLOCK_ANIMATION = new UnreleasedFlag(211);
-
-    /***************************************/
-    // 300 - power menu
-    public static final ReleasedFlag POWER_MENU_LITE =
-            new ReleasedFlag(300);
-
-    /***************************************/
-    // 400 - smartspace
-    public static final ReleasedFlag SMARTSPACE_DEDUPING =
-            new ReleasedFlag(400);
-
-    public static final ReleasedFlag SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
-            new ReleasedFlag(401);
-
-    public static final ResourceBooleanFlag SMARTSPACE =
-            new ResourceBooleanFlag(402, R.bool.flag_smartspace);
-
-    /***************************************/
-    // 500 - quick settings
-    /**
-     * @deprecated Not needed anymore
-     */
-    @Deprecated
-    public static final ReleasedFlag NEW_USER_SWITCHER =
-            new ReleasedFlag(500);
-
-    public static final UnreleasedFlag COMBINED_QS_HEADERS =
-            new UnreleasedFlag(501, true);
-
-    public static final ResourceBooleanFlag PEOPLE_TILE =
-            new ResourceBooleanFlag(502, R.bool.flag_conversations);
-
-    public static final ResourceBooleanFlag QS_USER_DETAIL_SHORTCUT =
-            new ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut);
-
-    /**
-     * @deprecated Not needed anymore
-     */
-    @Deprecated
-    public static final ReleasedFlag NEW_FOOTER = new ReleasedFlag(504);
-
-    public static final UnreleasedFlag NEW_HEADER = new UnreleasedFlag(505, true);
-    public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
-            new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
-
-    public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507);
-
-    /***************************************/
-    // 600- status bar
-    public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =
-            new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip);
-
-    public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
-            new ReleasedFlag(603, false);
-
-    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND =
-            new UnreleasedFlag(604, false);
-
-    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND =
-            new UnreleasedFlag(605, false);
-
-    /***************************************/
-    // 700 - dialer/calls
-    public static final ReleasedFlag ONGOING_CALL_STATUS_BAR_CHIP =
-            new ReleasedFlag(700);
-
-    public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE =
-            new ReleasedFlag(701);
-
-    public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP =
-            new ReleasedFlag(702);
-
-    /***************************************/
-    // 800 - general visual/theme
-    public static final ResourceBooleanFlag MONET =
-            new ResourceBooleanFlag(800, R.bool.flag_monet);
-
-    /***************************************/
-    // 801 - region sampling
-    public static final UnreleasedFlag REGION_SAMPLING = new UnreleasedFlag(801);
-
-    // 802 - wallpaper rendering
-    public static final UnreleasedFlag USE_CANVAS_RENDERER = new UnreleasedFlag(802, true);
-
-    // 803 - screen contents translation
-    public static final UnreleasedFlag SCREEN_CONTENTS_TRANSLATION = new UnreleasedFlag(803);
-
-    /***************************************/
-    // 900 - media
-    public static final ReleasedFlag MEDIA_TAP_TO_TRANSFER = new ReleasedFlag(900);
-    public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901);
-    public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903);
-    public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904);
-    public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905);
-    public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906);
-    public static final UnreleasedFlag UMO_SURFACE_RIPPLE = new UnreleasedFlag(907);
-
-    // 1000 - dock
-    public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING =
-            new ReleasedFlag(1000);
-    public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001);
-
-    public static final UnreleasedFlag ROUNDED_BOX_RIPPLE =
-            new UnreleasedFlag(1002, /* teamfood= */ true);
-
-    public static final UnreleasedFlag REFACTORED_DOCK_SETUP = new UnreleasedFlag(1003, true);
-
-    // 1100 - windowing
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS =
-            new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false);
-
-    /**
-     * b/170163464: animate bubbles expanded view collapse with home gesture
-     */
-    @Keep
-    public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE =
-            new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true);
-
-    @Keep
-    public static final DeviceConfigBooleanFlag WM_ENABLE_PARTIAL_SCREEN_SHARING =
-            new DeviceConfigBooleanFlag(1102, "record_task_content",
-                    NAMESPACE_WINDOW_MANAGER, false, true);
-
-    @Keep
-    public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW =
-            new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false);
-
-    @Keep
-    public static final SysPropBooleanFlag WM_DESKTOP_WINDOWING =
-            new SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false);
-
-    @Keep
-    public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL =
-            new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false);
-
-    @Keep
-    public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED =
-            new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false);
-
-    @Keep
-    public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
-            new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
-
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE =
-            new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true);
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
-            new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
-
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
-            new SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false);
-
-    // 1200 - predictive back
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
-            1200, "persist.wm.debug.predictive_back", true);
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK_ANIM = new SysPropBooleanFlag(
-            1201, "persist.wm.debug.predictive_back_anim", false);
-    @Keep
-    public static final SysPropBooleanFlag WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
-            new SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false);
-
-    public static final UnreleasedFlag NEW_BACK_AFFORDANCE =
-            new UnreleasedFlag(1203, false /* teamfood */);
-
-    // 1300 - screenshots
-
-    public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300);
-    public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301);
-
-    // 1400 - columbus
-    public static final ReleasedFlag QUICK_TAP_IN_PCC = new ReleasedFlag(1400);
-
-    // 1500 - chooser
-    public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500);
-
-    // 1600 - accessibility
-    public static final UnreleasedFlag A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
-            new UnreleasedFlag(1600);
-
-    // 1700 - clipboard
-    public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700);
-
-    // Pay no attention to the reflection behind the curtain.
-    // ========================== Curtain ==========================
-    // |                                                           |
-    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
-    private static Map<Integer, Flag<?>> sFlagMap;
-    static Map<Integer, Flag<?>> collectFlags() {
-        if (sFlagMap != null) {
-            return sFlagMap;
-        }
-
-        Map<Integer, Flag<?>> flags = new HashMap<>();
-        List<Field> flagFields = getFlagFields();
-
-        for (Field field : flagFields) {
-            try {
-                Flag<?> flag = (Flag<?>) field.get(null);
-                flags.put(flag.getId(), flag);
-            } catch (IllegalAccessException e) {
-                // no-op
-            }
-        }
-
-        sFlagMap = flags;
-
-        return sFlagMap;
-    }
-
-    static List<Field> getFlagFields() {
-        Field[] fields = Flags.class.getFields();
-        List<Field> result = new ArrayList<>();
-
-        for (Field field : fields) {
-            Class<?> t = field.getType();
-            if (Flag.class.isAssignableFrom(t)) {
-                result.add(field);
-            }
-        }
-
-        return result;
-    }
-    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
-    // |                                                           |
-    // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
-
-}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
new file mode 100644
index 0000000..9bd3cb1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2021 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.flags
+
+import android.provider.DeviceConfig
+import com.android.internal.annotations.Keep
+import com.android.systemui.R
+import java.lang.reflect.Field
+
+/**
+ * List of [Flag] objects for use in SystemUI.
+ *
+ * Flag Ids are integers. Ids must be unique. This is enforced in a unit test. Ids need not be
+ * sequential. Flags can "claim" a chunk of ids for flags in related features with a comment. This
+ * is purely for organizational purposes.
+ *
+ * On public release builds, flags will always return their default value. There is no way to change
+ * their value on release builds.
+ *
+ * See [FeatureFlagsDebug] for instructions on flipping the flags via adb.
+ */
+object Flags {
+    @JvmField val TEAMFOOD = UnreleasedFlag(1)
+
+    // 100 - notification
+    // TODO(b/254512751): Tracking Bug
+    val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = UnreleasedFlag(103)
+
+    // TODO(b/254512732): Tracking Bug
+    @JvmField val NSSL_DEBUG_LINES = UnreleasedFlag(105)
+
+    // TODO(b/254512505): Tracking Bug
+    @JvmField val NSSL_DEBUG_REMOVE_ANIMATION = UnreleasedFlag(106)
+
+    // TODO(b/254512624): Tracking Bug
+    @JvmField
+    val NOTIFICATION_DRAG_TO_CONTENTS =
+        ResourceBooleanFlag(108, R.bool.config_notificationToContents)
+
+    // TODO(b/254512517): Tracking Bug
+    val FSI_REQUIRES_KEYGUARD = UnreleasedFlag(110, teamfood = true)
+
+    // TODO(b/254512538): Tracking Bug
+    val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true)
+
+    // TODO(b/254512425): Tracking Bug
+    val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = false)
+
+    // TODO(b/254512731): Tracking Bug
+    @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true)
+    val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true)
+    val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true)
+    // next id: 116
+
+    // 200 - keyguard/lockscreen
+    // ** Flag retired **
+    // public static final BooleanFlag KEYGUARD_LAYOUT =
+    //         new BooleanFlag(200, true);
+    // TODO(b/254512713): Tracking Bug
+    @JvmField val LOCKSCREEN_ANIMATIONS = ReleasedFlag(201)
+
+    // TODO(b/254512750): Tracking Bug
+    val NEW_UNLOCK_SWIPE_ANIMATION = ReleasedFlag(202)
+    val CHARGING_RIPPLE = ResourceBooleanFlag(203, R.bool.flag_charging_ripple)
+
+    // TODO(b/254512281): Tracking Bug
+    @JvmField
+    val BOUNCER_USER_SWITCHER = ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher)
+
+    // TODO(b/254512694): Tracking Bug
+    val FACE_SCANNING_ANIM = ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation)
+
+    // TODO(b/254512676): Tracking Bug
+    @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = UnreleasedFlag(207, teamfood = true)
+
+    /**
+     * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual
+     * replacement of KeyguardBouncer.java.
+     */
+    // TODO(b/254512385): Tracking Bug
+    @JvmField val MODERN_BOUNCER = UnreleasedFlag(208)
+
+    /**
+     * Whether the user interactor and repository should use `UserSwitcherController`.
+     *
+     * If this is `false`, the interactor and repo skip the controller and directly access the
+     * framework APIs.
+     */
+    // TODO(b/254513286): Tracking Bug
+    val USER_INTERACTOR_AND_REPO_USE_CONTROLLER = UnreleasedFlag(210)
+
+    /**
+     * Whether `UserSwitcherController` should use the user interactor.
+     *
+     * When this is `true`, the controller does not directly access framework APIs. Instead, it goes
+     * through the interactor.
+     *
+     * Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it
+     * would created a cycle between controller -> interactor -> controller.
+     */
+    // TODO(b/254513102): Tracking Bug
+    val USER_CONTROLLER_USES_INTERACTOR = ReleasedFlag(211)
+
+    /**
+     * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
+     * the digits when the clock moves.
+     */
+    @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212)
+
+    // 300 - power menu
+    // TODO(b/254512600): Tracking Bug
+    @JvmField val POWER_MENU_LITE = ReleasedFlag(300)
+
+    // 400 - smartspace
+
+    // TODO(b/254513100): Tracking Bug
+    val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = ReleasedFlag(401)
+    val SMARTSPACE = ResourceBooleanFlag(402, R.bool.flag_smartspace)
+
+    // 500 - quick settings
+    @Deprecated("Not needed anymore") val NEW_USER_SWITCHER = ReleasedFlag(500)
+
+    // TODO(b/254512321): Tracking Bug
+    @JvmField val COMBINED_QS_HEADERS = UnreleasedFlag(501, teamfood = true)
+    val PEOPLE_TILE = ResourceBooleanFlag(502, R.bool.flag_conversations)
+    @JvmField
+    val QS_USER_DETAIL_SHORTCUT =
+        ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut)
+
+    // TODO(b/254512699): Tracking Bug
+    @Deprecated("Not needed anymore") val NEW_FOOTER = ReleasedFlag(504)
+
+    // TODO(b/254512747): Tracking Bug
+    val NEW_HEADER = UnreleasedFlag(505, teamfood = true)
+
+    // TODO(b/254512383): Tracking Bug
+    @JvmField
+    val FULL_SCREEN_USER_SWITCHER =
+        ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher)
+
+    // TODO(b/254512678): Tracking Bug
+    @JvmField val NEW_FOOTER_ACTIONS = ReleasedFlag(507)
+
+    // 600- status bar
+    // TODO(b/254513246): Tracking Bug
+    val STATUS_BAR_USER_SWITCHER = ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip)
+
+    // TODO(b/254513025): Tracking Bug
+    val STATUS_BAR_LETTERBOX_APPEARANCE = ReleasedFlag(603, teamfood = false)
+
+    // TODO(b/254512623): Tracking Bug
+    @Deprecated("Replaced by mobile and wifi specific flags.")
+    val NEW_STATUS_BAR_PIPELINE_BACKEND = UnreleasedFlag(604, teamfood = false)
+
+    // TODO(b/254512660): Tracking Bug
+    @Deprecated("Replaced by mobile and wifi specific flags.")
+    val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false)
+
+    val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606, false)
+
+    val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607, false)
+
+    // 700 - dialer/calls
+    // TODO(b/254512734): Tracking Bug
+    val ONGOING_CALL_STATUS_BAR_CHIP = ReleasedFlag(700)
+
+    // TODO(b/254512681): Tracking Bug
+    val ONGOING_CALL_IN_IMMERSIVE = ReleasedFlag(701)
+
+    // TODO(b/254512753): Tracking Bug
+    val ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = ReleasedFlag(702)
+
+    // 800 - general visual/theme
+    @JvmField val MONET = ResourceBooleanFlag(800, R.bool.flag_monet)
+
+    // 801 - region sampling
+    // TODO(b/254512848): Tracking Bug
+    val REGION_SAMPLING = UnreleasedFlag(801)
+
+    // 802 - wallpaper rendering
+    // TODO(b/254512923): Tracking Bug
+    @JvmField val USE_CANVAS_RENDERER = UnreleasedFlag(802, teamfood = true)
+
+    // 803 - screen contents translation
+    // TODO(b/254513187): Tracking Bug
+    val SCREEN_CONTENTS_TRANSLATION = UnreleasedFlag(803)
+
+    // 804 - monochromatic themes
+    @JvmField val MONOCHROMATIC_THEMES = UnreleasedFlag(804)
+
+    // 900 - media
+    // TODO(b/254512697): Tracking Bug
+    val MEDIA_TAP_TO_TRANSFER = ReleasedFlag(900)
+
+    // TODO(b/254512502): Tracking Bug
+    val MEDIA_SESSION_ACTIONS = UnreleasedFlag(901)
+
+    // TODO(b/254512726): Tracking Bug
+    val MEDIA_NEARBY_DEVICES = ReleasedFlag(903)
+
+    // TODO(b/254512695): Tracking Bug
+    val MEDIA_MUTE_AWAIT = ReleasedFlag(904)
+
+    // TODO(b/254512654): Tracking Bug
+    @JvmField val DREAM_MEDIA_COMPLICATION = UnreleasedFlag(905)
+
+    // TODO(b/254512673): Tracking Bug
+    @JvmField val DREAM_MEDIA_TAP_TO_OPEN = UnreleasedFlag(906)
+
+    // TODO(b/254513168): Tracking Bug
+    val UMO_SURFACE_RIPPLE = UnreleasedFlag(907)
+
+    // 1000 - dock
+    val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000)
+
+    // TODO(b/254512444): Tracking Bug
+    @JvmField val DOCK_SETUP_ENABLED = ReleasedFlag(1001)
+
+    // TODO(b/254512758): Tracking Bug
+    @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002)
+
+    // TODO(b/254512525): Tracking Bug
+    @JvmField val REFACTORED_DOCK_SETUP = ReleasedFlag(1003, teamfood = true)
+
+    // 1100 - windowing
+    @Keep
+    val WM_ENABLE_SHELL_TRANSITIONS =
+        SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false)
+
+    /** b/170163464: animate bubbles expanded view collapse with home gesture */
+    @Keep
+    val BUBBLES_HOME_GESTURE =
+        SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true)
+
+    // TODO(b/254513207): Tracking Bug
+    @JvmField
+    @Keep
+    val WM_ENABLE_PARTIAL_SCREEN_SHARING =
+        DeviceConfigBooleanFlag(
+            1102,
+            "record_task_content",
+            DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+            false,
+            teamfood = true
+        )
+
+    // TODO(b/254512674): Tracking Bug
+    @JvmField
+    @Keep
+    val HIDE_NAVBAR_WINDOW = SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false)
+
+    @Keep
+    val WM_DESKTOP_WINDOWING = SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false)
+
+    @Keep
+    val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false)
+
+    @Keep
+    val FLOATING_TASKS_ENABLED = SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false)
+
+    @Keep
+    val SHOW_FLOATING_TASKS_AS_BUBBLES =
+        SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false)
+
+    @Keep
+    val ENABLE_FLING_TO_DISMISS_BUBBLE =
+        SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true)
+
+    @Keep
+    val ENABLE_FLING_TO_DISMISS_PIP =
+        SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true)
+
+    @Keep
+    val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
+        SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false)
+
+    // 1200 - predictive back
+    @Keep
+    val WM_ENABLE_PREDICTIVE_BACK =
+        SysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", true)
+
+    @Keep
+    val WM_ENABLE_PREDICTIVE_BACK_ANIM =
+        SysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", false)
+
+    @Keep
+    val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
+        SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false)
+
+    // TODO(b/254512728): Tracking Bug
+    @JvmField val NEW_BACK_AFFORDANCE = UnreleasedFlag(1203, teamfood = false)
+
+    // 1300 - screenshots
+    // TODO(b/254512719): Tracking Bug
+    @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300)
+
+    // TODO(b/254513155): Tracking Bug
+    @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301)
+
+    // 1400 - columbus
+    // TODO(b/254512756): Tracking Bug
+    val QUICK_TAP_IN_PCC = ReleasedFlag(1400)
+
+    // 1500 - chooser
+    // TODO(b/254512507): Tracking Bug
+    val CHOOSER_UNBUNDLED = UnreleasedFlag(1500)
+
+    // 1600 - accessibility
+    @JvmField val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS = UnreleasedFlag(1600)
+
+    // 1700 - clipboard
+    @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700)
+
+    // 1800 - shade container
+    @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true)
+
+    // Pay no attention to the reflection behind the curtain.
+    // ========================== Curtain ==========================
+    // |                                                           |
+    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
+    @JvmStatic
+    fun collectFlags(): Map<Int, Flag<*>> {
+        return flagFields
+            .map { field ->
+                // field[null] returns the current value of the field.
+                // See java.lang.Field#get
+                val flag = field[null] as Flag<*>
+                flag.id to flag
+            }
+            .toMap()
+    }
+
+    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
+    @JvmStatic
+    val flagFields: List<Field>
+        get() {
+            return Flags::class.java.fields.filter { f ->
+                Flag::class.java.isAssignableFrom(f.type)
+            }
+        }
+    // |                                                           |
+    // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
+}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index da5819a..3ef5499 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -116,6 +116,7 @@
 import com.android.systemui.MultiListLayout.MultiListAdapter;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -448,10 +449,11 @@
      *
      * @param keyguardShowing     True if keyguard is showing
      * @param isDeviceProvisioned True if device is provisioned
-     * @param view                The view from which we should animate the dialog when showing it
+     * @param expandable          The expandable from which we should animate the dialog when
+     *                            showing it
      */
     public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned,
-            @Nullable View view) {
+            @Nullable Expandable expandable) {
         mKeyguardShowing = keyguardShowing;
         mDeviceProvisioned = isDeviceProvisioned;
         if (mDialog != null && mDialog.isShowing()) {
@@ -463,7 +465,7 @@
             mDialog.dismiss();
             mDialog = null;
         } else {
-            handleShow(view);
+            handleShow(expandable);
         }
     }
 
@@ -495,7 +497,7 @@
         }
     }
 
-    protected void handleShow(@Nullable View view) {
+    protected void handleShow(@Nullable Expandable expandable) {
         awakenIfNecessary();
         mDialog = createDialog();
         prepareDialog();
@@ -507,10 +509,12 @@
         // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports
         mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
 
-        if (view != null) {
-            mDialogLaunchAnimator.showFromView(mDialog, view,
-                    new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG));
+        DialogLaunchAnimator.Controller controller =
+                expandable != null ? expandable.dialogLaunchController(
+                        new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG)) : null;
+        if (controller != null) {
+            mDialogLaunchAnimator.show(mDialog, controller);
         } else {
             mDialog.show();
         }
@@ -1016,8 +1020,9 @@
                             Log.w(TAG, "Bugreport handler could not be launched");
                             mIActivityManager.requestInteractiveBugReport();
                         }
-                        // Close shade so user sees the activity
-                        mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade);
+                        // Maybe close shade (depends on a flag) so user sees the activity
+                        mCentralSurfacesOptional.ifPresent(
+                                CentralSurfaces::collapseShadeForBugreport);
                     } catch (RemoteException e) {
                     }
                 }
@@ -1036,8 +1041,8 @@
                 mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL);
                 mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS);
                 mIActivityManager.requestFullBugReport();
-                // Close shade so user sees the activity
-                mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade);
+                // Maybe close shade (depends on a flag) so user sees the activity
+                mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShadeForBugreport);
             } catch (RemoteException e) {
             }
             return false;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 84bd8ce..0d74dc8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -401,6 +401,11 @@
     private final float mWindowCornerRadius;
 
     /**
+     * The duration in milliseconds of the dream open animation.
+     */
+    private final int mDreamOpenAnimationDuration;
+
+    /**
      * The animation used for hiding keyguard. This is used to fetch the animation timings if
      * WindowManager is not providing us with them.
      */
@@ -751,6 +756,7 @@
             if (DEBUG) Log.d(TAG, "keyguardGone");
             mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false);
             mKeyguardDisplayManager.hide();
+            mUpdateMonitor.startBiometricWatchdog();
             Trace.endSection();
         }
 
@@ -946,8 +952,7 @@
                         }
 
                         mOccludeByDreamAnimator = ValueAnimator.ofFloat(0f, 1f);
-                        // Use the same duration as for the UNOCCLUDE.
-                        mOccludeByDreamAnimator.setDuration(UNOCCLUDE_ANIMATION_DURATION);
+                        mOccludeByDreamAnimator.setDuration(mDreamOpenAnimationDuration);
                         mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR);
                         mOccludeByDreamAnimator.addUpdateListener(
                                 animation -> {
@@ -1179,6 +1184,9 @@
         mPowerButtonY = context.getResources().getDimensionPixelSize(
                 R.dimen.physical_power_button_center_screen_location_y);
         mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+
+        mDreamOpenAnimationDuration = context.getResources().getInteger(
+                com.android.internal.R.integer.config_dreamOpenAnimationDuration);
     }
 
     public void userActivity() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 56f1ac4..56a1f1a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -43,6 +43,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
+import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -72,6 +73,7 @@
             FalsingModule.class,
             KeyguardQuickAffordanceModule.class,
             KeyguardRepositoryModule.class,
+            StartKeyguardTransitionModule.class,
         })
 public class KeyguardModule {
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 45b668e..b186ae0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.common.shared.model.Position
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.doze.DozeHost
+import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
@@ -85,6 +86,9 @@
      */
     val dozeAmount: Flow<Float>
 
+    /** Observable for the [StatusBarState] */
+    val statusBarState: Flow<StatusBarState>
+
     /**
      * Returns `true` if the keyguard is showing; `false` otherwise.
      *
@@ -185,6 +189,24 @@
         return keyguardStateController.isShowing
     }
 
+    override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow {
+        val callback =
+            object : StatusBarStateController.StateListener {
+                override fun onStateChanged(state: Int) {
+                    trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state")
+                }
+            }
+
+        statusBarStateController.addCallback(callback)
+        trySendWithFailureLogging(
+            statusBarStateIntToObject(statusBarStateController.getState()),
+            TAG,
+            "initial state"
+        )
+
+        awaitClose { statusBarStateController.removeCallback(callback) }
+    }
+
     override fun setAnimateDozingTransitions(animate: Boolean) {
         _animateBottomAreaDozingTransitions.value = animate
     }
@@ -197,6 +219,15 @@
         _clockPosition.value = Position(x, y)
     }
 
+    private fun statusBarStateIntToObject(value: Int): StatusBarState {
+        return when (value) {
+            0 -> StatusBarState.SHADE
+            1 -> StatusBarState.KEYGUARD
+            2 -> StatusBarState.SHADE_LOCKED
+            else -> throw IllegalArgumentException("Invalid StatusBarState value: $value")
+        }
+    }
+
     companion object {
         private const val TAG = "KeyguardRepositoryImpl"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
new file mode 100644
index 0000000..e8532ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.annotation.FloatRange
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filter
+
+@SysUISingleton
+class KeyguardTransitionRepository @Inject constructor() {
+    /*
+     * Each transition between [KeyguardState]s will have an associated Flow.
+     * In order to collect these events, clients should call [transition].
+     */
+    private val _transitions = MutableStateFlow(TransitionStep())
+    val transitions = _transitions.asStateFlow()
+
+    /* Information about the active transition. */
+    private var currentTransitionInfo: TransitionInfo? = null
+    /*
+     * When manual control of the transition is requested, a unique [UUID] is used as the handle
+     * to permit calls to [updateTransition]
+     */
+    private var updateTransitionId: UUID? = null
+
+    /**
+     * Interactors that require information about changes between [KeyguardState]s will call this to
+     * register themselves for flowable [TransitionStep]s when that transition occurs.
+     */
+    fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
+        return transitions.filter { step -> step.from == from && step.to == to }
+    }
+
+    /**
+     * Begin a transition from one state to another. The [info.from] must match
+     * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid
+     * unplanned transitions.
+     */
+    fun startTransition(info: TransitionInfo): UUID? {
+        if (currentTransitionInfo != null) {
+            // Open questions:
+            // * Queue of transitions? buffer of 1?
+            // * Are transitions cancellable if a new one is triggered?
+            // * What validation does this need to do?
+            Log.wtf(TAG, "Transition still active: $currentTransitionInfo")
+            return null
+        }
+        currentTransitionInfo?.animator?.cancel()
+
+        currentTransitionInfo = info
+        info.animator?.let { animator ->
+            // An animator was provided, so use it to run the transition
+            animator.setFloatValues(0f, 1f)
+            val updateListener =
+                object : AnimatorUpdateListener {
+                    override fun onAnimationUpdate(animation: ValueAnimator) {
+                        emitTransition(
+                            info,
+                            (animation.getAnimatedValue() as Float),
+                            TransitionState.RUNNING
+                        )
+                    }
+                }
+            val adapter =
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationStart(animation: Animator) {
+                        Log.i(TAG, "Starting transition: $info")
+                        emitTransition(info, 0f, TransitionState.STARTED)
+                    }
+                    override fun onAnimationCancel(animation: Animator) {
+                        Log.i(TAG, "Cancelling transition: $info")
+                    }
+                    override fun onAnimationEnd(animation: Animator) {
+                        Log.i(TAG, "Ending transition: $info")
+                        emitTransition(info, 1f, TransitionState.FINISHED)
+                        animator.removeListener(this)
+                        animator.removeUpdateListener(updateListener)
+                    }
+                }
+            animator.addListener(adapter)
+            animator.addUpdateListener(updateListener)
+            animator.start()
+            return@startTransition null
+        }
+            ?: run {
+                Log.i(TAG, "Starting transition (manual): $info")
+                emitTransition(info, 0f, TransitionState.STARTED)
+
+                // No animator, so it's manual. Provide a mechanism to callback
+                updateTransitionId = UUID.randomUUID()
+                return@startTransition updateTransitionId
+            }
+    }
+
+    /**
+     * Allows manual control of a transition. When calling [startTransition], the consumer must pass
+     * in a null animator. In return, it will get a unique [UUID] that will be validated to allow
+     * further updates.
+     *
+     * When the transition is over, TransitionState.FINISHED must be passed into the [state]
+     * parameter.
+     */
+    fun updateTransition(
+        transitionId: UUID,
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        state: TransitionState
+    ) {
+        if (updateTransitionId != transitionId) {
+            Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
+            return
+        }
+
+        if (currentTransitionInfo == null) {
+            Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'")
+            return
+        }
+
+        currentTransitionInfo?.let { info ->
+            if (state == TransitionState.FINISHED) {
+                updateTransitionId = null
+                Log.i(TAG, "Ending transition: $info")
+            }
+
+            emitTransition(info, value, state)
+        }
+    }
+
+    private fun emitTransition(
+        info: TransitionInfo,
+        value: Float,
+        transitionState: TransitionState
+    ) {
+        if (transitionState == TransitionState.FINISHED) {
+            currentTransitionInfo = null
+        }
+        _transitions.value = TransitionStep(info.from, info.to, value, transitionState)
+    }
+
+    companion object {
+        private const val TAG = "KeyguardTransitionRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..4003766
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.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.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class AodLockscreenTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardRepository: KeyguardRepository,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor("AOD<->LOCKSCREEN") {
+
+    override fun start() {
+        scope.launch {
+            keyguardRepository.isDozing.collect { isDozing ->
+                if (isDozing) {
+                    keyguardTransitionRepository.startTransition(
+                        TransitionInfo(
+                            name,
+                            KeyguardState.LOCKSCREEN,
+                            KeyguardState.AOD,
+                            getAnimator(),
+                        )
+                    )
+                } else {
+                    keyguardTransitionRepository.startTransition(
+                        TransitionInfo(
+                            name,
+                            KeyguardState.AOD,
+                            KeyguardState.LOCKSCREEN,
+                            getAnimator(),
+                        )
+                    )
+                }
+            }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 500L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
index 7d4db37..2af9318 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
@@ -273,8 +273,8 @@
     /** Tell the bouncer to start the pre hide animation. */
     fun startDisappearAnimation(runnable: Runnable) {
         val finishRunnable = Runnable {
-            repository.setStartDisappearAnimation(null)
             runnable.run()
+            repository.setStartDisappearAnimation(null)
         }
         repository.setStartDisappearAnimation(finishRunnable)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 192919e..fc2269c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -38,7 +38,7 @@
     val dozeAmount: Flow<Float> = repository.dozeAmount
     /** Whether the system is in doze mode. */
     val isDozing: Flow<Boolean> = repository.isDozing
-    /** Whether the keyguard is showing ot not. */
+    /** Whether the keyguard is showing to not. */
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
 
     fun isKeyguardShowing(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
new file mode 100644
index 0000000..b166681
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import java.util.Set
+import javax.inject.Inject
+
+@SysUISingleton
+class KeyguardTransitionCoreStartable
+@Inject
+constructor(
+    private val interactors: Set<TransitionInteractor>,
+) : CoreStartable {
+
+    override fun start() {
+        // By listing the interactors in a when, the compiler will help enforce all classes
+        // extending the sealed class [TransitionInteractor] will be initialized.
+        interactors.forEach {
+            // `when` needs to be an expression in order for the compiler to enforce it being
+            // exhaustive
+            val ret =
+                when (it) {
+                    is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+                }
+            it.start()
+        }
+    }
+
+    companion object {
+        private const val TAG = "KeyguardTransitionCoreStartable"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
new file mode 100644
index 0000000..59bb22786
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -0,0 +1,37 @@
+/*
+ *  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.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates business-logic related to the keyguard transitions. */
+@SysUISingleton
+class KeyguardTransitionInteractor
+@Inject
+constructor(
+    repository: KeyguardTransitionRepository,
+) {
+    /** AOD->LOCKSCREEN transition information. */
+    val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
new file mode 100644
index 0000000..3c2a12e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class LockscreenBouncerTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardRepository: KeyguardRepository,
+    private val shadeRepository: ShadeRepository,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor("LOCKSCREEN<->BOUNCER") {
+
+    private var transitionId: UUID? = null
+
+    override fun start() {
+        scope.launch {
+            shadeRepository.shadeModel.sample(
+                combine(
+                    keyguardTransitionRepository.transitions,
+                    keyguardRepository.statusBarState,
+                ) { transitions, statusBarState ->
+                    Pair(transitions, statusBarState)
+                }
+            ) { shadeModel, pair ->
+                val (transitions, statusBarState) = pair
+
+                val id = transitionId
+                if (id != null) {
+                    // An existing `id` means a transition is started, and calls to
+                    // `updateTransition` will control it until FINISHED
+                    keyguardTransitionRepository.updateTransition(
+                        id,
+                        shadeModel.expansionAmount,
+                        if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) {
+                            transitionId = null
+                            TransitionState.FINISHED
+                        } else {
+                            TransitionState.RUNNING
+                        }
+                    )
+                } else {
+                    // TODO (b/251849525): Remove statusbarstate check when that state is integrated
+                    // into KeyguardTransitionRepository
+                    val isOnLockscreen =
+                        transitions.transitionState == TransitionState.FINISHED &&
+                            transitions.to == KeyguardState.LOCKSCREEN
+                    if (
+                        isOnLockscreen &&
+                            shadeModel.isUserDragging &&
+                            statusBarState != SHADE_LOCKED
+                    ) {
+                        transitionId =
+                            keyguardTransitionRepository.startTransition(
+                                TransitionInfo(
+                                    ownerName = name,
+                                    from = KeyguardState.LOCKSCREEN,
+                                    to = KeyguardState.BOUNCER,
+                                    animator = null,
+                                )
+                            )
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
new file mode 100644
index 0000000..74c542c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.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.keyguard.domain.interactor
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+
+@Module
+abstract class StartKeyguardTransitionModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(KeyguardTransitionCoreStartable::class)
+    abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenBouncer(
+        impl: LockscreenBouncerTransitionInteractor
+    ): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
new file mode 100644
index 0000000..a2a46d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.keyguard.domain.interactor
+/**
+ * Each TransitionInteractor is responsible for determining under which conditions to notify
+ * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is
+ * determined by [KeyguardTransitionRepository].
+ *
+ * [name] field should be a unique identifiable string representing this state, used primarily for
+ * logging
+ *
+ * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the
+ * 'when' clause of [KeyguardTransitionCoreStartable]
+ */
+sealed class TransitionInteractor(val name: String) {
+
+    abstract fun start()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
new file mode 100644
index 0000000..f66d5d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.keyguard.shared.model
+
+/** List of all possible states to transition to/from */
+enum class KeyguardState {
+    /** For initialization only */
+    NONE,
+    /* Always-on Display. The device is in a low-power mode with a minimal UI visible */
+    AOD,
+    /*
+     * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS
+     * (Fingerprint Sensor) variations, for the user to verify their credentials
+     */
+    BOUNCER,
+    /*
+     * Device is actively displaying keyguard UI and is not in low-power mode. Device may be
+     * unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
+     */
+    LOCKSCREEN,
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
similarity index 71%
copy from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
index 23072a2..bb95347 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
@@ -11,11 +11,13 @@
  * 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.
+ * limitations under the License
  */
+package com.android.systemui.keyguard.shared.model
 
-package com.android.settingslib.spa.gallery
-
-import com.android.settingslib.spa.framework.DebugActivity
-
-class GalleryDebugActivity : DebugActivity()
+/** See [com.android.systemui.statusbar.StatusBarState] for definitions */
+enum class StatusBarState {
+    SHADE,
+    KEYGUARD,
+    SHADE_LOCKED,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt
new file mode 100644
index 0000000..bfccf3fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.keyguard.shared.model
+
+import android.animation.ValueAnimator
+
+/** Tracks who is controlling the current transition, and how to run it. */
+data class TransitionInfo(
+    val ownerName: String,
+    val from: KeyguardState,
+    val to: KeyguardState,
+    val animator: ValueAnimator?, // 'null' animator signal manual control
+) {
+    override fun toString(): String =
+        "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " +
+            (if (animator != null) {
+                "animated"
+            } else {
+                "manual"
+            }) +
+            ")"
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
similarity index 71%
copy from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
index 23072a2..d8691c1 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
@@ -11,11 +11,14 @@
  * 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.
+ * limitations under the License
  */
+package com.android.systemui.keyguard.shared.model
 
-package com.android.settingslib.spa.gallery
-
-import com.android.settingslib.spa.framework.DebugActivity
-
-class GalleryDebugActivity : DebugActivity()
+/** Possible states for a running transition between [State] */
+enum class TransitionState {
+    NONE,
+    STARTED,
+    RUNNING,
+    FINISHED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
new file mode 100644
index 0000000..688ec91
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.keyguard.shared.model
+
+/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */
+data class TransitionStep(
+    val from: KeyguardState = KeyguardState.NONE,
+    val to: KeyguardState = KeyguardState.NONE,
+    val value: Float = 0f, // constrained [0.0, 1.0]
+    val transitionState: TransitionState = TransitionState.NONE,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
index 5651399..f9e341c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
@@ -19,6 +19,9 @@
 import android.app.ActivityManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
+
 import javax.inject.Inject
 
 @SysUISingleton
@@ -26,7 +29,7 @@
     private val dumpManager: DumpManager,
     private val logcatEchoTracker: LogcatEchoTracker
 ) {
-    /* limit the size of maxPoolSize for low ram (Go) devices */
+    /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */
     private fun adjustMaxSize(requestedMaxSize: Int): Int {
         return if (ActivityManager.isLowRamDeviceStatic()) {
             minOf(requestedMaxSize, 20) /* low ram max log size*/
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
index 7f1ad6d..eeadf40 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
@@ -23,7 +23,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link com.android.systemui.log.LogBuffer} for BiometricMessages processing such as
+ * A {@link com.android.systemui.plugins.log.LogBuffer} for BiometricMessages processing such as
  * {@link com.android.systemui.biometrics.FaceHelpMessageDeferral}
  */
 @Qualifier
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
index 7d1f1c2..5cca1ab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
index 9ca0293..1d016d8 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
index 7c5f402..c9f78bc 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
index 08d969b..76d20be 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 28aa19e..00bf210 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -22,11 +22,11 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.LogBufferFactory;
-import com.android.systemui.log.LogcatEchoTracker;
-import com.android.systemui.log.LogcatEchoTrackerDebug;
-import com.android.systemui.log.LogcatEchoTrackerProd;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogcatEchoTracker;
+import com.android.systemui.plugins.log.LogcatEchoTrackerDebug;
+import com.android.systemui.plugins.log.LogcatEchoTrackerProd;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.util.Compile;
 
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
index 1d7ba94..90ced02 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
index b03655a..e5ac3e2 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
index c67d8be..73690ab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
index 53963fc..99ec05b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
index 5c572e8..1570d43 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
index edab8c3..bf216c6 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
index 75a34fc..8c904ea 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
index b1c6dcf..6d91f0c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
index 20fc6ff..26af496 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
index fcc184a..61daf9c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
index 760fbf3..a59afa0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
index a0b6864..6f8ea7f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
index 8c8753a..835d349 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
index 7259eeb..6e2bd7b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
index e96e532..77b1bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
index 557a254..9fd166b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
index dd5010c..dd168ba 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
index bd0d298..d24bfcb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
index b237f2d..67cdb72 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
index f26b316..af0f7c5 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
index dd68375..4c276e2 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
index 8671dbf..ba8b27c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
index b1018f9..d40624b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.media
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaCarouselControllerLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** A debug logger for [MediaCarouselController]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index 1ac2a07..be357ee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -182,8 +182,7 @@
 
     override fun shouldGetOnlyDefaultActivities() = false
 
-    // TODO(b/240924732) flip the flag when the recents selector is ready
-    override fun shouldShowContentPreview() = false
+    override fun shouldShowContentPreview() = true
 
     override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
         recentsViewController.createView(parent)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
index b52565d..cc06b6c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.people.widget.PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.Utils
 import com.android.systemui.util.time.SystemClock
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
index d9c58c0..8c9e2d8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
@@ -18,11 +18,10 @@
 
 import android.media.session.PlaybackState
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaTimeoutListenerLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
-
 private const val TAG = "MediaTimeout"
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
index 73868189..51c658c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.media
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaViewLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "MediaView"
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
index 41f7354..a9c5c61 100644
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
@@ -18,9 +18,9 @@
 
 import android.content.ComponentName
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaBrowserLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** A logger for events in [ResumeMediaBrowser]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index a8a8433..e15e2d3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -17,7 +17,6 @@
 package com.android.systemui.media.dagger;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer;
 import com.android.systemui.log.dagger.MediaTttSenderLogBuffer;
 import com.android.systemui.media.MediaDataManager;
@@ -33,6 +32,7 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger;
 import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger;
 import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.util.Optional;
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
index 78f4e01..5ace3ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
@@ -1,9 +1,9 @@
 package com.android.systemui.media.muteawait
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaMuteAwaitLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [MediaMuteAwaitConnectionManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
index 46b2cc14..78408fc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
@@ -1,9 +1,9 @@
 package com.android.systemui.media.nearby
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NearbyMediaDevicesLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [NearbyMediaDevicesManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index b565f3c..38c971e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.media.taptotransfer.common
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.temporarydisplay.TemporaryViewLogger
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index c3de94f..0a60437 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -21,6 +21,8 @@
 import android.graphics.drawable.Drawable
 import com.android.settingslib.Utils
 import com.android.systemui.R
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 
 /** Utility methods for media tap-to-transfer. */
 class MediaTttUtils {
@@ -31,6 +33,23 @@
         const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED"
 
         /**
+         * Returns the information needed to display the icon in [Icon] form.
+         *
+         * See [getIconInfoFromPackageName].
+         */
+        fun getIconFromPackageName(
+            context: Context,
+            appPackageName: String?,
+            logger: MediaTttLogger,
+        ): Icon {
+            val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger)
+            return Icon.Loaded(
+                iconInfo.drawable,
+                ContentDescription.Loaded(iconInfo.contentDescription)
+            )
+        }
+
+        /**
          * Returns the information needed to display the icon.
          *
          * The information will either contain app name and icon of the app playing media, or a
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index c24b030..6e596ee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -18,17 +18,12 @@
 
 import android.app.StatusBarManager
 import android.content.Context
-import android.media.MediaRoute2Info
 import android.util.Log
-import android.view.View
 import androidx.annotation.StringRes
 import com.android.internal.logging.UiEventLogger
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
-import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
-import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 
 /**
  * A class enumerating all the possible states of the media tap-to-transfer chip on the sender
@@ -38,6 +33,7 @@
  * @property stringResId the res ID of the string that should be displayed in the chip. Null if the
  *   state should not have the chip be displayed.
  * @property transferStatus the transfer status that the chip state represents.
+ * @property endItem the item that should be displayed in the end section of the chip.
  * @property timeout the amount of time this chip should display on the screen before it times out
  *   and disappears.
  */
@@ -46,6 +42,7 @@
     val uiEvent: UiEventLogger.UiEventEnum,
     @StringRes val stringResId: Int?,
     val transferStatus: TransferStatus,
+    val endItem: SenderEndItem?,
     val timeout: Long = DEFAULT_TIMEOUT_MILLIS
 ) {
     /**
@@ -58,6 +55,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST,
         R.string.media_move_closer_to_start_cast,
         transferStatus = TransferStatus.NOT_STARTED,
+        endItem = null,
     ),
 
     /**
@@ -71,6 +69,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST,
         R.string.media_move_closer_to_end_cast,
         transferStatus = TransferStatus.NOT_STARTED,
+        endItem = null,
     ),
 
     /**
@@ -82,6 +81,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED,
         R.string.media_transfer_playing_different_device,
         transferStatus = TransferStatus.IN_PROGRESS,
+        endItem = SenderEndItem.Loading,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
     ),
 
@@ -94,6 +94,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
         R.string.media_transfer_playing_this_device,
         transferStatus = TransferStatus.IN_PROGRESS,
+        endItem = SenderEndItem.Loading,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
     ),
 
@@ -105,36 +106,13 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED,
         R.string.media_transfer_playing_different_device,
         transferStatus = TransferStatus.SUCCEEDED,
-    ) {
-        override fun undoClickListener(
-            chipbarCoordinator: ChipbarCoordinator,
-            routeInfo: MediaRoute2Info,
-            undoCallback: IUndoMediaTransferCallback?,
-            uiEventLogger: MediaTttSenderUiEventLogger,
-            falsingManager: FalsingManager,
-        ): View.OnClickListener? {
-            if (undoCallback == null) {
-                return null
-            }
-            return View.OnClickListener {
-                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
-
-                uiEventLogger.logUndoClicked(
-                    MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED
-                )
-                undoCallback.onUndoTriggered()
-                // The external service should eventually send us a TransferToThisDeviceTriggered
-                // state, but that may take too long to go through the binder and the user may be
-                // confused as to why the UI hasn't changed yet. So, we immediately change the UI
-                // here.
-                chipbarCoordinator.displayView(
-                    ChipSenderInfo(
-                        TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback
-                    )
-                )
-            }
-        }
-    },
+        endItem = SenderEndItem.UndoButton(
+            uiEventOnClick =
+            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED,
+            newState =
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED
+        ),
+    ),
 
     /**
      * A state representing that a transfer back to this device has been successfully completed.
@@ -144,36 +122,13 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
         R.string.media_transfer_playing_this_device,
         transferStatus = TransferStatus.SUCCEEDED,
-    ) {
-        override fun undoClickListener(
-            chipbarCoordinator: ChipbarCoordinator,
-            routeInfo: MediaRoute2Info,
-            undoCallback: IUndoMediaTransferCallback?,
-            uiEventLogger: MediaTttSenderUiEventLogger,
-            falsingManager: FalsingManager,
-        ): View.OnClickListener? {
-            if (undoCallback == null) {
-                return null
-            }
-            return View.OnClickListener {
-                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
-
-                uiEventLogger.logUndoClicked(
-                    MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED
-                )
-                undoCallback.onUndoTriggered()
-                // The external service should eventually send us a TransferToReceiverTriggered
-                // state, but that may take too long to go through the binder and the user may be
-                // confused as to why the UI hasn't changed yet. So, we immediately change the UI
-                // here.
-                chipbarCoordinator.displayView(
-                    ChipSenderInfo(
-                        TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback
-                    )
-                )
-            }
-        }
-    },
+        endItem = SenderEndItem.UndoButton(
+            uiEventOnClick =
+            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED,
+            newState =
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED
+        ),
+    ),
 
     /** A state representing that a transfer to the receiver device has failed. */
     TRANSFER_TO_RECEIVER_FAILED(
@@ -181,6 +136,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED,
         R.string.media_transfer_failed,
         transferStatus = TransferStatus.FAILED,
+        endItem = SenderEndItem.Error,
     ),
 
     /** A state representing that a transfer back to this device has failed. */
@@ -189,6 +145,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED,
         R.string.media_transfer_failed,
         transferStatus = TransferStatus.FAILED,
+        endItem = SenderEndItem.Error,
     ),
 
     /** A state representing that this device is far away from any receiver device. */
@@ -197,37 +154,27 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER,
         stringResId = null,
         transferStatus = TransferStatus.TOO_FAR,
-    );
+        // We shouldn't be displaying the chipbar anyway
+        endItem = null,
+    ) {
+        override fun getChipTextString(context: Context, otherDeviceName: String): Text {
+            // TODO(b/245610654): Better way to handle this.
+            throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " +
+                "so its string should never be fetched")
+        }
+    };
 
     /**
      * Returns a fully-formed string with the text that the chip should display.
      *
+     * Throws an NPE if [stringResId] is null.
+     *
      * @param otherDeviceName the name of the other device involved in the transfer.
      */
-    fun getChipTextString(context: Context, otherDeviceName: String): String? {
-        if (stringResId == null) {
-            return null
-        }
-        return context.getString(stringResId, otherDeviceName)
+    open fun getChipTextString(context: Context, otherDeviceName: String): Text {
+        return Text.Loaded(context.getString(stringResId!!, otherDeviceName))
     }
 
-    /**
-     * Returns a click listener for the undo button on the chip. Returns null if this chip state
-     * doesn't have an undo button.
-     *
-     * @param chipbarCoordinator passed as a parameter in case we want to display a new chipbar
-     *   when undo is clicked.
-     * @param undoCallback if present, the callback that should be called when the user clicks the
-     *   undo button. The undo button will only be shown if this is non-null.
-     */
-    open fun undoClickListener(
-        chipbarCoordinator: ChipbarCoordinator,
-        routeInfo: MediaRoute2Info,
-        undoCallback: IUndoMediaTransferCallback?,
-        uiEventLogger: MediaTttSenderUiEventLogger,
-        falsingManager: FalsingManager,
-    ): View.OnClickListener? = null
-
     companion object {
         /**
          * Returns the sender state enum associated with the given [displayState] from
@@ -253,6 +200,26 @@
     }
 }
 
+/** Represents the item that should be displayed in the end section of the chip. */
+sealed class SenderEndItem {
+    /** A loading icon should be displayed. */
+    object Loading : SenderEndItem()
+
+    /** An error icon should be displayed. */
+    object Error : SenderEndItem()
+
+    /**
+     * An undo button should be displayed.
+     *
+     * @property uiEventOnClick the UI event to log when this button is clicked.
+     * @property newState the state that should immediately be transitioned to.
+     */
+    data class UndoButton(
+        val uiEventOnClick: UiEventLogger.UiEventEnum,
+        @StatusBarManager.MediaTransferSenderState val newState: Int,
+    ) : SenderEndItem()
+}
+
 // Give the Transfer*Triggered states a longer timeout since those states represent an active
 // process and we should keep the user informed about it as long as possible (but don't allow it to
 // continue indefinitely).
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index 224303a..edf759d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -20,14 +20,20 @@
 import android.content.Context
 import android.media.MediaRoute2Info
 import android.util.Log
+import android.view.View
+import com.android.internal.logging.UiEventLogger
 import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
+import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
 import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG
 import javax.inject.Inject
 
@@ -107,7 +113,90 @@
             chipbarCoordinator.removeView(removalReason)
         } else {
             displayedState = chipState
-            chipbarCoordinator.displayView(ChipSenderInfo(chipState, routeInfo, undoCallback))
+            chipbarCoordinator.displayView(
+                createChipbarInfo(
+                    chipState,
+                    routeInfo,
+                    undoCallback,
+                    context,
+                    logger,
+                )
+            )
         }
     }
+
+    /**
+     * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display.
+     */
+    private fun createChipbarInfo(
+        chipStateSender: ChipStateSender,
+        routeInfo: MediaRoute2Info,
+        undoCallback: IUndoMediaTransferCallback?,
+        context: Context,
+        logger: MediaTttLogger,
+    ): ChipbarInfo {
+        val packageName = routeInfo.clientPackageName
+        val otherDeviceName = routeInfo.name.toString()
+
+        return ChipbarInfo(
+            // Display the app's icon as the start icon
+            startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger),
+            text = chipStateSender.getChipTextString(context, otherDeviceName),
+            endItem =
+                when (chipStateSender.endItem) {
+                    null -> null
+                    is SenderEndItem.Loading -> ChipbarEndItem.Loading
+                    is SenderEndItem.Error -> ChipbarEndItem.Error
+                    is SenderEndItem.UndoButton -> {
+                        if (undoCallback != null) {
+                            getUndoButton(
+                                undoCallback,
+                                chipStateSender.endItem.uiEventOnClick,
+                                chipStateSender.endItem.newState,
+                                routeInfo,
+                            )
+                        } else {
+                            null
+                        }
+                    }
+                },
+            vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
+        )
+    }
+
+    /**
+     * Returns an undo button for the chip.
+     *
+     * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and
+     * this coordinator will transition to [newState].
+     */
+    private fun getUndoButton(
+        undoCallback: IUndoMediaTransferCallback,
+        uiEvent: UiEventLogger.UiEventEnum,
+        @StatusBarManager.MediaTransferSenderState newState: Int,
+        routeInfo: MediaRoute2Info,
+    ): ChipbarEndItem.Button {
+        val onClickListener =
+            View.OnClickListener {
+                uiEventLogger.logUndoClicked(uiEvent)
+                undoCallback.onUndoTriggered()
+
+                // The external service should eventually send us a new TransferTriggered state, but
+                // but that may take too long to go through the binder and the user may be confused
+                // as to why the UI hasn't changed yet. So, we immediately change the UI here.
+                updateMediaTapToTransferSenderDisplay(
+                    newState,
+                    routeInfo,
+                    // Since we're force-updating the UI, we don't have any [undoCallback] from the
+                    // external service (and TransferTriggered states don't have undo callbacks
+                    // anyway).
+                    undoCallback = null,
+                )
+            }
+
+        return ChipbarEndItem.Button(
+            Text.Resource(R.string.media_transfer_undo),
+            onClickListener,
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
index f15720d..b963809 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
@@ -16,16 +16,36 @@
 
 package com.android.systemui.media.taptotransfer.sender
 
-/** Represents the different possible transfer states that we could be in. */
-enum class TransferStatus {
+import android.os.VibrationEffect
+
+/**
+ * Represents the different possible transfer states that we could be in and the vibration effects
+ * that come with updating transfer states.
+ *
+ * @property vibrationEffect an optional vibration effect when the transfer status is changed.
+ */
+enum class TransferStatus(
+    val vibrationEffect: VibrationEffect? = null,
+) {
     /** The transfer hasn't started yet. */
-    NOT_STARTED,
+    NOT_STARTED(
+        vibrationEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1.0f, 0)
+                .compose()
+    ),
     /** The transfer is currently ongoing but hasn't completed yet. */
-    IN_PROGRESS,
+    IN_PROGRESS(
+        vibrationEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 0)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.7f, 70)
+                .compose(),
+    ),
     /** The transfer has completed successfully. */
     SUCCEEDED,
     /** The transfer has completed with a failure. */
-    FAILED,
+    FAILED(vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)),
     /** The device is too far away to do a transfer. */
     TOO_FAR,
 }
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index 1ea9347..03503fd 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -17,10 +17,10 @@
 package com.android.systemui.privacy.logging
 
 import android.permission.PermissionGroupUsage
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.PrivacyLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.privacy.PrivacyDialog
 import com.android.systemui.privacy.PrivacyItem
 import java.text.SimpleDateFormat
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 482a139..bb2b441 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -52,6 +52,7 @@
 import com.android.systemui.R
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -98,10 +99,10 @@
     fun init()
 
     /**
-     * Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if
+     * Show the foreground services dialog. The dialog will be expanded from [expandable] if
      * it's not `null`.
      */
-    fun showDialog(viewLaunchedFrom: View?)
+    fun showDialog(expandable: Expandable?)
 
     /** Add a [OnNumberOfPackagesChangedListener]. */
     fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
@@ -367,7 +368,7 @@
 
     override fun shouldUpdateFooterVisibility() = dialog == null
 
-    override fun showDialog(viewLaunchedFrom: View?) {
+    override fun showDialog(expandable: Expandable?) {
         synchronized(lock) {
             if (dialog == null) {
 
@@ -403,16 +404,18 @@
                 }
 
                 mainExecutor.execute {
-                    viewLaunchedFrom
-                        ?.let {
-                            dialogLaunchAnimator.showFromView(
-                                dialog, it,
-                                cuj = DialogCuj(
-                                    InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                                    INTERACTION_JANK_TAG
-                                )
+                    val controller =
+                        expandable?.dialogLaunchController(
+                            DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG,
                             )
-                        } ?: dialog.show()
+                        )
+                    if (controller != null) {
+                        dialogLaunchAnimator.show(dialog, controller)
+                    } else {
+                        dialog.show()
+                    }
                 }
 
                 backgroundExecutor.execute {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
index 9d64781..a9943e8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
@@ -32,6 +32,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -156,7 +157,7 @@
             startSettingsActivity()
         } else if (v === powerMenuLite) {
             uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
-            globalActionsDialog?.showOrHideDialog(false, true, v)
+            globalActionsDialog?.showOrHideDialog(false, true, Expandable.fromView(powerMenuLite))
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
index 7511278e..b1b9dd7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
@@ -29,6 +29,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.qs.dagger.QSScope;
@@ -130,7 +131,7 @@
 
     @Override
     public void onClick(View view) {
-        mFgsManagerController.showDialog(mRootView);
+        mFgsManagerController.showDialog(Expandable.fromView(view));
     }
 
     public void refreshState() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
index e5d86cc..025fb22 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.qs
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.QSFragmentDisableLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
index 67bf300..6c1e956 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
@@ -39,6 +39,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.systemui.FontSizeUtils;
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.common.shared.model.Icon;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -169,7 +170,7 @@
 
     // TODO(b/242040009): Remove this.
     public void showDeviceMonitoringDialog() {
-        mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView);
+        mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, Expandable.fromView(mView));
     }
 
     public void refreshState() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
index ae6ed20..67bc769 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
@@ -75,6 +75,7 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.common.shared.model.ContentDescription;
 import com.android.systemui.common.shared.model.Icon;
 import com.android.systemui.dagger.SysUISingleton;
@@ -190,8 +191,9 @@
     }
 
     /** Show the device monitoring dialog. */
-    public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) {
-        createDialog(quickSettingsContext, view);
+    public void showDeviceMonitoringDialog(Context quickSettingsContext,
+            @Nullable Expandable expandable) {
+        createDialog(quickSettingsContext, expandable);
     }
 
     /**
@@ -440,7 +442,7 @@
         }
     }
 
-    private void createDialog(Context quickSettingsContext, @Nullable View view) {
+    private void createDialog(Context quickSettingsContext, @Nullable Expandable expandable) {
         mShouldUseSettingsButton.set(false);
         mBgHandler.post(() -> {
             String settingsButtonText = getSettingsButton();
@@ -453,9 +455,12 @@
                         ? settingsButtonText : getNegativeButton(), this);
 
                 mDialog.setView(dialogView);
-                if (view != null && view.isAggregatedVisible()) {
-                    mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj(
-                            InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG));
+                DialogLaunchAnimator.Controller controller =
+                        expandable != null ? expandable.dialogLaunchController(new DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG))
+                                : null;
+                if (controller != null) {
+                    mDialogLaunchAnimator.show(mDialog, controller);
                 } else {
                     mDialog.show();
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index ac46c85..f37d668 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -34,10 +34,12 @@
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dumpable;
+import com.android.systemui.ProtoDumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.nano.SystemUIProtoDump;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
@@ -48,6 +50,7 @@
 import com.android.systemui.qs.external.TileServiceKey;
 import com.android.systemui.qs.external.TileServiceRequestController;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.nano.QsTileState;
 import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
@@ -59,16 +62,20 @@
 import com.android.systemui.util.leak.GarbageMonitor;
 import com.android.systemui.util.settings.SecureSettings;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -82,7 +89,7 @@
  * This class also provides the interface for adding/removing/changing tiles.
  */
 @SysUISingleton
-public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable {
+public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable {
     private static final String TAG = "QSTileHost";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final int MAX_QS_INSTANCE_ID = 1 << 20;
@@ -671,4 +678,15 @@
         mTiles.values().stream().filter(obj -> obj instanceof Dumpable)
                 .forEach(o -> ((Dumpable) o).dump(pw, args));
     }
+
+    @Override
+    public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) {
+        List<QsTileState> data = mTiles.values().stream()
+                .map(QSTile::getState)
+                .map(TileStateToProtoKt::toProto)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        systemUIProtoDump.tiles = data.toArray(new QsTileState[0]);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt
new file mode 100644
index 0000000..2c8a5a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.qs
+
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.external.CustomTile
+import com.android.systemui.qs.nano.QsTileState
+import com.android.systemui.util.nano.ComponentNameProto
+
+fun QSTile.State.toProto(): QsTileState? {
+    if (TextUtils.isEmpty(spec)) return null
+    val state = QsTileState()
+    if (spec.startsWith(CustomTile.PREFIX)) {
+        val protoComponentName = ComponentNameProto()
+        val tileComponentName = CustomTile.getComponentFromSpec(spec)
+        protoComponentName.packageName = tileComponentName.packageName
+        protoComponentName.className = tileComponentName.className
+        state.componentName = protoComponentName
+    } else {
+        state.spec = spec
+    }
+    state.state =
+        when (this.state) {
+            Tile.STATE_UNAVAILABLE -> QsTileState.UNAVAILABLE
+            Tile.STATE_INACTIVE -> QsTileState.INACTIVE
+            Tile.STATE_ACTIVE -> QsTileState.ACTIVE
+            else -> QsTileState.UNAVAILABLE
+        }
+    label?.let { state.label = it.toString() }
+    secondaryLabel?.let { state.secondaryLabel = it.toString() }
+    if (this is QSTile.BooleanState) {
+        state.booleanState = value
+    }
+    return state
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index 4cacbba..5d03da3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -35,6 +35,7 @@
 
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.settings.UserTracker;
@@ -53,6 +54,7 @@
 /**
  * Runs the day-to-day operations of which tiles should be bound and when.
  */
+@SysUISingleton
 public class TileServices extends IQSService.Stub {
     static final int DEFAULT_MAX_BOUND = 3;
     static final int REDUCED_MAX_BOUND = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
index cf9b41c..9ba3501 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
@@ -23,13 +23,11 @@
 import android.content.IntentFilter
 import android.os.UserHandle
 import android.provider.Settings
-import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.logging.nano.MetricsProto
 import com.android.internal.util.FrameworkStatsLog
-import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
@@ -74,37 +72,27 @@
     val deviceMonitoringDialogRequests: Flow<Unit>
 
     /**
-     * Show the device monitoring dialog, expanded from [view].
-     *
-     * Important: [view] must be associated to the same [Context] as the [Quick Settings fragment]
-     * [com.android.systemui.qs.QSFragment].
-     */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showDeviceMonitoringDialog(view: View)
-
-    /**
-     * Show the device monitoring dialog.
+     * Show the device monitoring dialog, expanded from [expandable] if it's not null.
      *
      * Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings
      * fragment][com.android.systemui.qs.QSFragment].
      */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showDeviceMonitoringDialog(quickSettingsContext: Context)
+    fun showDeviceMonitoringDialog(quickSettingsContext: Context, expandable: Expandable?)
 
     /** Show the foreground services dialog. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showForegroundServicesDialog(view: View)
+    fun showForegroundServicesDialog(expandable: Expandable)
 
     /** Show the power menu dialog. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View)
+    fun showPowerMenuDialog(
+        globalActionsDialogLite: GlobalActionsDialogLite,
+        expandable: Expandable,
+    )
 
     /** Show the settings. */
     fun showSettings(expandable: Expandable)
 
     /** Show the user switcher. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showUserSwitcher(view: View)
+    fun showUserSwitcher(context: Context, expandable: Expandable)
 }
 
 @SysUISingleton
@@ -147,28 +135,32 @@
             null,
         )
 
-    override fun showDeviceMonitoringDialog(view: View) {
-        qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view)
-        DevicePolicyEventLogger.createEvent(
-                FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
-            )
-            .write()
+    override fun showDeviceMonitoringDialog(
+        quickSettingsContext: Context,
+        expandable: Expandable?,
+    ) {
+        qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, expandable)
+        if (expandable != null) {
+            DevicePolicyEventLogger.createEvent(
+                    FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
+                )
+                .write()
+        }
     }
 
-    override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
-        qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null)
+    override fun showForegroundServicesDialog(expandable: Expandable) {
+        fgsManagerController.showDialog(expandable)
     }
 
-    override fun showForegroundServicesDialog(view: View) {
-        fgsManagerController.showDialog(view)
-    }
-
-    override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) {
+    override fun showPowerMenuDialog(
+        globalActionsDialogLite: GlobalActionsDialogLite,
+        expandable: Expandable,
+    ) {
         uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
         globalActionsDialogLite.showOrHideDialog(
             /* keyguardShowing= */ false,
             /* isDeviceProvisioned= */ true,
-            view,
+            expandable,
         )
     }
 
@@ -189,21 +181,21 @@
         )
     }
 
-    override fun showUserSwitcher(view: View) {
+    override fun showUserSwitcher(context: Context, expandable: Expandable) {
         if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
-            userSwitchDialogController.showDialog(view)
+            userSwitchDialogController.showDialog(context, expandable)
             return
         }
 
         val intent =
-            Intent(view.context, UserSwitcherActivity::class.java).apply {
+            Intent(context, UserSwitcherActivity::class.java).apply {
                 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
             }
 
         activityStarter.startActivity(
             intent,
             true /* dismissShade */,
-            ActivityLaunchAnimator.Controller.fromView(view, null),
+            expandable.activityLaunchController(),
             true /* showOverlockscreenwhenlocked */,
             UserHandle.SYSTEM,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index dd1ffcc..3e39c8e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -31,6 +31,7 @@
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.R
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.people.ui.view.PeopleViewBinder.bind
@@ -125,7 +126,7 @@
                 launch {
                     viewModel.security.collect { security ->
                         if (previousSecurity != security) {
-                            bindSecurity(securityHolder, security)
+                            bindSecurity(view.context, securityHolder, security)
                             previousSecurity = security
                         }
                     }
@@ -159,6 +160,7 @@
     }
 
     private fun bindSecurity(
+        quickSettingsContext: Context,
         securityHolder: TextButtonViewHolder,
         security: FooterActionsSecurityButtonViewModel?,
     ) {
@@ -171,9 +173,12 @@
         // Make sure that the chevron is visible and that the button is clickable if there is a
         // listener.
         val chevron = securityHolder.chevron
-        if (security.onClick != null) {
+        val onClick = security.onClick
+        if (onClick != null) {
             securityView.isClickable = true
-            securityView.setOnClickListener(security.onClick)
+            securityView.setOnClickListener {
+                onClick(quickSettingsContext, Expandable.fromView(securityView))
+            }
             chevron.isVisible = true
         } else {
             securityView.isClickable = false
@@ -205,7 +210,9 @@
             foregroundServicesWithNumberView.isVisible = false
 
             foregroundServicesWithTextView.isVisible = true
-            foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick)
+            foregroundServicesWithTextView.setOnClickListener {
+                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+            }
             foregroundServicesWithTextHolder.text.text = foregroundServices.text
             foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges
         } else {
@@ -213,7 +220,9 @@
             foregroundServicesWithTextView.isVisible = false
 
             foregroundServicesWithNumberView.visibility = View.VISIBLE
-            foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick)
+            foregroundServicesWithNumberView.setOnClickListener {
+                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+            }
             foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
             foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
             foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges
@@ -229,7 +238,7 @@
         }
 
         buttonView.setBackgroundResource(model.background)
-        buttonView.setOnClickListener(model.onClick)
+        buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
 
         val icon = model.icon
         val iconView = button.icon
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
index 9b5f683..8d819da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.footer.ui.viewmodel
 
 import android.annotation.DrawableRes
-import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 
 /**
@@ -29,7 +29,5 @@
     val icon: Icon,
     val iconTint: Int?,
     @DrawableRes val background: Int,
-    // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
-    // or activity.
-    val onClick: (View) -> Unit,
+    val onClick: (Expandable) -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
index 98b53cb..ff8130d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.qs.footer.ui.viewmodel
 
-import android.view.View
+import com.android.systemui.animation.Expandable
 
 /** A ViewModel for the foreground services button. */
 data class FooterActionsForegroundServicesButtonViewModel(
@@ -24,5 +24,5 @@
     val text: String,
     val displayText: Boolean,
     val hasNewChanges: Boolean,
-    val onClick: (View) -> Unit,
+    val onClick: (Expandable) -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
index 98ab129..3450505 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
@@ -16,12 +16,13 @@
 
 package com.android.systemui.qs.footer.ui.viewmodel
 
-import android.view.View
+import android.content.Context
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 
 /** A ViewModel for the security button. */
 data class FooterActionsSecurityButtonViewModel(
     val icon: Icon,
     val text: String,
-    val onClick: ((View) -> Unit)?,
+    val onClick: ((quickSettingsContext: Context, Expandable) -> Unit)?,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index d3c06f6..dee6fad 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.util.Log
-import android.view.View
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -199,50 +198,51 @@
      */
     suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
         footerActionsInteractor.deviceMonitoringDialogRequests.collect {
-            footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext)
+            footerActionsInteractor.showDeviceMonitoringDialog(
+                quickSettingsContext,
+                expandable = null,
+            )
         }
     }
 
-    private fun onSecurityButtonClicked(view: View) {
+    private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showDeviceMonitoringDialog(view)
+        footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable)
     }
 
-    private fun onForegroundServiceButtonClicked(view: View) {
+    private fun onForegroundServiceButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showForegroundServicesDialog(view)
+        footerActionsInteractor.showForegroundServicesDialog(expandable)
     }
 
-    private fun onUserSwitcherClicked(view: View) {
+    private fun onUserSwitcherClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showUserSwitcher(view)
+        footerActionsInteractor.showUserSwitcher(context, expandable)
     }
 
-    // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
-    // or activity.
-    private fun onSettingsButtonClicked(view: View) {
+    private fun onSettingsButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showSettings(Expandable.fromView(view))
+        footerActionsInteractor.showSettings(expandable)
     }
 
-    private fun onPowerButtonClicked(view: View) {
+    private fun onPowerButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view)
+        footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable)
     }
 
     private fun userSwitcherButton(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 6038006..931dc8d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.qs.logging
 
 import android.service.quicksettings.Tile
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.QSLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.statusbar.StatusBarState
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto
new file mode 100644
index 0000000..2a61033
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package com.android.systemui.qs;
+
+import "frameworks/base/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto";
+
+option java_multiple_files = true;
+
+message QsTileState {
+  oneof identifier {
+    string spec = 1;
+    com.android.systemui.util.ComponentNameProto component_name = 2;
+  }
+
+  enum State {
+    UNAVAILABLE = 0;
+    INACTIVE = 1;
+    ACTIVE = 2;
+  }
+
+  State state = 3;
+  oneof optional_boolean_state {
+    bool boolean_state = 4;
+  }
+  oneof optional_label {
+    string label = 5;
+  }
+  oneof optional_secondary_label {
+    string secondary_label = 6;
+  }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index d2d5063..b6b657e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -26,6 +26,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
@@ -43,6 +44,9 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.user.data.source.UserRecord;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 import javax.inject.Inject;
 
 /**
@@ -83,6 +87,13 @@
         private final FalsingManager mFalsingManager;
         private @Nullable UserSwitchDialogController.DialogShower mDialogShower;
 
+        @NonNull
+        @Override
+        protected List<UserRecord> getUsers() {
+            return super.getUsers().stream().filter(
+                    userRecord -> !userRecord.isManageUsers).collect(Collectors.toList());
+        }
+
         @Inject
         public Adapter(Context context, UserSwitcherController controller,
                 UiEventLogger uiEventLogger, FalsingManager falsingManager) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index bdcc6b0..314252b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -23,13 +23,13 @@
 import android.content.Intent
 import android.provider.Settings
 import android.view.LayoutInflater
-import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.R
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -77,10 +77,10 @@
      * Show a [UserDialog].
      *
      * Populate the dialog with information from and adapter obtained from
-     * [userDetailViewAdapterProvider] and show it as launched from [view].
+     * [userDetailViewAdapterProvider] and show it as launched from [expandable].
      */
-    fun showDialog(view: View) {
-        with(dialogFactory(view.context)) {
+    fun showDialog(context: Context, expandable: Expandable) {
+        with(dialogFactory(context)) {
             setShowForAllUsers(true)
             setCanceledOnTouchOutside(true)
 
@@ -112,13 +112,19 @@
 
             adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid))
 
-            dialogLaunchAnimator.showFromView(
-                this, view,
-                cuj = DialogCuj(
-                    InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                    INTERACTION_JANK_TAG
+            val controller =
+                expandable.dialogLaunchController(
+                    DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
                 )
-            )
+            if (controller != null) {
+                dialogLaunchAnimator.show(
+                    this,
+                    controller,
+                )
+            } else {
+                show()
+            }
+
             uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN)
             adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator))
         }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index a494f42..6b540aa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -292,6 +292,7 @@
             clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
                 val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
                 v.pivotX = newPivot
+                v.pivotY = v.height.toFloat() / 2
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
index 07e8b9f..754036d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
@@ -16,7 +16,7 @@
 import android.view.MotionEvent
 import com.android.systemui.dump.DumpsysTableLogger
 import com.android.systemui.dump.Row
-import com.android.systemui.util.collection.RingBuffer
+import com.android.systemui.plugins.util.RingBuffer
 import java.text.SimpleDateFormat
 import java.util.Locale
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 29d546f..42e8753 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -151,6 +151,8 @@
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
+import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.FalsingManager.FalsingTapListener;
@@ -583,6 +585,7 @@
     private final SysUiState mSysUiState;
 
     private final NotificationShadeDepthController mDepthController;
+    private final NavigationBarController mNavigationBarController;
     private final int mDisplayId;
 
     private KeyguardIndicationController mKeyguardIndicationController;
@@ -861,6 +864,7 @@
             PrivacyDotViewController privacyDotViewController,
             TapAgainViewController tapAgainViewController,
             NavigationModeController navigationModeController,
+            NavigationBarController navigationBarController,
             FragmentService fragmentService,
             ContentResolver contentResolver,
             RecordingController recordingController,
@@ -954,6 +958,7 @@
         mNotificationsQSContainerController = notificationsQSContainerController;
         mNotificationListContainer = notificationListContainer;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
+        mNavigationBarController = navigationBarController;
         mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider;
         mNotificationsQSContainerController.init();
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
@@ -1443,6 +1448,16 @@
         mMaxAllowedKeyguardNotifications = maxAllowed;
     }
 
+    @VisibleForTesting
+    boolean getClosing() {
+        return mClosing;
+    }
+
+    @VisibleForTesting
+    boolean getIsFlinging() {
+        return mIsFlinging;
+    }
+
     private void updateMaxDisplayedNotifications(boolean recompute) {
         if (recompute) {
             setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1));
@@ -1675,9 +1690,9 @@
                 transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
 
                 boolean customClockAnimation =
-                        mKeyguardStatusViewController
-                                .getClockAnimations()
-                                .getHasCustomPositionUpdatedAnimation();
+                            mKeyguardStatusViewController.getClockAnimations() != null
+                            && mKeyguardStatusViewController.getClockAnimations()
+                                    .getHasCustomPositionUpdatedAnimation();
 
                 if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) {
                     // Find the clock, so we can exclude it from this transition.
@@ -2127,7 +2142,8 @@
         animator.start();
     }
 
-    private void onFlingEnd(boolean cancelled) {
+    @VisibleForTesting
+    void onFlingEnd(boolean cancelled) {
         mIsFlinging = false;
         // No overshoot when the animation ends
         setOverExpansionInternal(0, false /* isFromGesture */);
@@ -2670,12 +2686,16 @@
             mQsExpanded = expanded;
             updateQsState();
             updateExpandedHeightToMaxHeight();
-            mFalsingCollector.setQsExpanded(expanded);
-            mCentralSurfaces.setQsExpanded(expanded);
-            mNotificationsQSContainerController.setQsExpanded(expanded);
-            mPulseExpansionHandler.setQsExpanded(expanded);
-            mKeyguardBypassController.setQSExpanded(expanded);
-            mPrivacyDotViewController.setQsExpanded(expanded);
+            setStatusAccessibilityImportance(expanded
+                    ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                    : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+            updateSystemUiStateFlags();
+            NavigationBarView navigationBarView =
+                    mNavigationBarController.getNavigationBarView(mDisplayId);
+            if (navigationBarView != null) {
+                navigationBarView.onStatusBarPanelStateChanged();
+            }
+            mShadeExpansionStateManager.onQsExpansionChanged(expanded);
         }
     }
 
@@ -3717,6 +3737,11 @@
         setListening(true);
     }
 
+    @VisibleForTesting
+    void setTouchSlopExceeded(boolean isTouchSlopExceeded) {
+        mTouchSlopExceeded = isTouchSlopExceeded;
+    }
+
     public void setOverExpansion(float overExpansion) {
         if (overExpansion == mOverExpansion) {
             return;
@@ -3868,12 +3893,14 @@
         }
     }
 
-    private void setIsClosing(boolean isClosing) {
+    @VisibleForTesting
+    void setIsClosing(boolean isClosing) {
         boolean wasClosing = isClosing();
         mClosing = isClosing;
         if (wasClosing != isClosing) {
             mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing);
         }
+        mAmbientState.setIsClosing(isClosing);
     }
 
     private void updateDozingVisibilities(boolean animate) {
@@ -3903,12 +3930,16 @@
         switch (mBarState) {
             case KEYGUARD:
                 if (!mDozingOnDown) {
-                    if (mUpdateMonitor.isFaceEnrolled()
-                            && !mUpdateMonitor.isFaceDetectionRunning()
-                            && !mUpdateMonitor.getUserCanSkipBouncer(
-                            KeyguardUpdateMonitor.getCurrentUser())) {
-                        mUpdateMonitor.requestFaceAuth(true,
-                                FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+                    mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false");
+                    // Try triggering face auth, this "might" run. Check
+                    // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run.
+                    boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true,
+                            FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+
+                    if (didFaceAuthRun) {
+                        mUpdateMonitor.requestActiveUnlock(
+                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                                "lockScreenEmptySpaceTap");
                     } else {
                         mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT,
                                 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
@@ -3916,11 +3947,6 @@
                                 .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT);
                         startUnlockHintAnimation();
                     }
-                    if (mUpdateMonitor.isFaceEnrolled()) {
-                        mUpdateMonitor.requestActiveUnlock(
-                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
-                                "lockScreenEmptySpaceTap");
-                    }
                 }
                 return true;
             case StatusBarState.SHADE_LOCKED:
@@ -4664,14 +4690,16 @@
         Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
     }
 
-    private void notifyExpandingStarted() {
+    @VisibleForTesting
+    void notifyExpandingStarted() {
         if (!mExpanding) {
             mExpanding = true;
             onExpandingStarted();
         }
     }
 
-    private void notifyExpandingFinished() {
+    @VisibleForTesting
+    void notifyExpandingFinished() {
         endClosing();
         if (mExpanding) {
             mExpanding = false;
@@ -4771,6 +4799,7 @@
         mAmbientState.setSwipingUp(false);
         if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
                 || Math.abs(y - mInitialExpandY) > mTouchSlop
+                || (!isFullyExpanded() && !isFullyCollapsed())
                 || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
             mVelocityTracker.computeCurrentVelocity(1000);
             float vel = mVelocityTracker.getYVelocity();
@@ -5168,7 +5197,8 @@
      */
     public void updatePanelExpansionAndVisibility() {
         mShadeExpansionStateManager.onPanelExpansionChanged(
-                mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+                mExpandedFraction, isExpanded(),
+                mTracking, mExpansionDragDownAmountPx);
         updateVisibility();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 1d92105..66a22f4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -135,7 +135,8 @@
             DumpManager dumpManager,
             KeyguardStateController keyguardStateController,
             ScreenOffAnimationController screenOffAnimationController,
-            AuthController authController) {
+            AuthController authController,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         mContext = context;
         mWindowManager = windowManager;
         mActivityManager = activityManager;
@@ -156,6 +157,7 @@
                 .addCallback(mStateListener,
                         SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER);
         configurationController.addCallback(this);
+        shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
 
         float desiredPreferredRefreshRate = context.getResources()
                 .getInteger(R.integer.config_keyguardRefreshRate);
@@ -607,8 +609,7 @@
         apply(mCurrentState);
     }
 
-    @Override
-    public void setQsExpanded(boolean expanded) {
+    private void onQsExpansionChanged(Boolean expanded) {
         mCurrentState.mQsExpanded = expanded;
         apply(mCurrentState);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index d6f0de8..73c6d50 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -36,17 +36,12 @@
     private val navigationModeController: NavigationModeController,
     private val overviewProxyService: OverviewProxyService,
     private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController,
+    private val shadeExpansionStateManager: ShadeExpansionStateManager,
     private val featureFlags: FeatureFlags,
     @Main private val delayableExecutor: DelayableExecutor
 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
 
-    var qsExpanded = false
-        set(value) {
-            if (field != value) {
-                field = value
-                mView.invalidate()
-            }
-        }
+    private var qsExpanded = false
     private var splitShadeEnabled = false
     private var isQSDetailShowing = false
     private var isQSCustomizing = false
@@ -71,6 +66,13 @@
             taskbarVisible = visible
         }
     }
+    private val shadeQsExpansionListener: ShadeQsExpansionListener =
+        ShadeQsExpansionListener { isQsExpanded ->
+            if (qsExpanded != isQsExpanded) {
+                qsExpanded = isQsExpanded
+                mView.invalidate()
+            }
+        }
 
     // With certain configuration changes (like light/dark changes), the nav bar will disappear
     // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value
@@ -106,6 +108,7 @@
     public override fun onViewAttached() {
         updateResources()
         overviewProxyService.addCallback(taskbarVisibilityListener)
+        shadeExpansionStateManager.addQsExpansionListener(shadeQsExpansionListener)
         mView.setInsetsChangedListener(delayedInsetSetter)
         mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
         mView.setConfigurationChangedListener { updateResources() }
@@ -113,6 +116,7 @@
 
     override fun onViewDetached() {
         overviewProxyService.removeCallback(taskbarVisibilityListener)
+        shadeExpansionStateManager.removeQsExpansionListener(shadeQsExpansionListener)
         mView.removeOnInsetsChangedListener()
         mView.removeQSFragmentAttachedListener()
         mView.setConfigurationChangedListener(null)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index f617d47..7bba74a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.FloatRange
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.util.Compile
+import java.util.concurrent.CopyOnWriteArrayList
 import javax.inject.Inject
 
 /**
@@ -31,12 +32,14 @@
 @SysUISingleton
 class ShadeExpansionStateManager @Inject constructor() {
 
-    private val expansionListeners = mutableListOf<ShadeExpansionListener>()
-    private val stateListeners = mutableListOf<ShadeStateListener>()
+    private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
+    private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>()
+    private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>()
 
     @PanelState private var state: Int = STATE_CLOSED
     @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
     private var expanded: Boolean = false
+    private var qsExpanded: Boolean = false
     private var tracking: Boolean = false
     private var dragDownPxAmount: Float = 0f
 
@@ -57,6 +60,15 @@
         expansionListeners.remove(listener)
     }
 
+    fun addQsExpansionListener(listener: ShadeQsExpansionListener) {
+        qsExpansionListeners.add(listener)
+        listener.onQsExpansionChanged(qsExpanded)
+    }
+
+    fun removeQsExpansionListener(listener: ShadeQsExpansionListener) {
+        qsExpansionListeners.remove(listener)
+    }
+
     /** Adds a listener that will be notified when the panel state has changed. */
     fun addStateListener(listener: ShadeStateListener) {
         stateListeners.add(listener)
@@ -126,6 +138,14 @@
         expansionListeners.forEach { it.onPanelExpansionChanged(expansionChangeEvent) }
     }
 
+    /** Called when the quick settings expansion changes to fully expanded or collapsed. */
+    fun onQsExpansionChanged(qsExpanded: Boolean) {
+        this.qsExpanded = qsExpanded
+
+        debugLog("qsExpanded=$qsExpanded")
+        qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) }
+    }
+
     /** Updates the panel state if necessary. */
     fun updateState(@PanelState state: Int) {
         debugLog(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 7bee0ba..2b788d8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -1,10 +1,10 @@
 package com.android.systemui.shade
 
 import android.view.MotionEvent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.ShadeLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogMessage
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
@@ -12,64 +12,69 @@
 
 /** Lightweight logging utility for the Shade. */
 class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) {
-  fun v(@CompileTimeConstant msg: String) {
-    buffer.log(TAG, LogLevel.VERBOSE, msg)
-  }
+    fun v(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, LogLevel.VERBOSE, msg)
+    }
 
-  private inline fun log(
-      logLevel: LogLevel,
-      initializer: LogMessage.() -> Unit,
-      noinline printer: LogMessage.() -> String
-  ) {
-    buffer.log(TAG, logLevel, initializer, printer)
-  }
+    private inline fun log(
+        logLevel: LogLevel,
+        initializer: LogMessage.() -> Unit,
+        noinline printer: LogMessage.() -> String
+    ) {
+        buffer.log(TAG, logLevel, initializer, printer)
+    }
 
-  fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
-    log(
-        LogLevel.VERBOSE,
-        { double1 = h.toDouble() },
-        { "onQsIntercept: move action, QS tracking enabled. h = $double1" })
-  }
+    fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
+        log(
+            LogLevel.VERBOSE,
+            { double1 = h.toDouble() },
+            { "onQsIntercept: move action, QS tracking enabled. h = $double1" }
+        )
+    }
 
-  fun logQsTrackingNotStarted(
-      initialTouchY: Float,
-      y: Float,
-      h: Float,
-      touchSlop: Float,
-      qsExpanded: Boolean,
-      collapsedOnDown: Boolean,
-      keyguardShowing: Boolean,
-      qsExpansionEnabled: Boolean
-  ) {
-    log(
-        LogLevel.VERBOSE,
-        {
-          int1 = initialTouchY.toInt()
-          int2 = y.toInt()
-          long1 = h.toLong()
-          double1 = touchSlop.toDouble()
-          bool1 = qsExpanded
-          bool2 = collapsedOnDown
-          bool3 = keyguardShowing
-          bool4 = qsExpansionEnabled
-        },
-        {
-          "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded=" +
-              "$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4"
-        })
-  }
+    fun logQsTrackingNotStarted(
+        initialTouchY: Float,
+        y: Float,
+        h: Float,
+        touchSlop: Float,
+        qsExpanded: Boolean,
+        collapsedOnDown: Boolean,
+        keyguardShowing: Boolean,
+        qsExpansionEnabled: Boolean
+    ) {
+        log(
+            LogLevel.VERBOSE,
+            {
+                int1 = initialTouchY.toInt()
+                int2 = y.toInt()
+                long1 = h.toLong()
+                double1 = touchSlop.toDouble()
+                bool1 = qsExpanded
+                bool2 = collapsedOnDown
+                bool3 = keyguardShowing
+                bool4 = qsExpansionEnabled
+            },
+            {
+                "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded" +
+                    "=$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4"
+            }
+        )
+    }
 
-  fun logMotionEvent(event: MotionEvent, message: String) {
-    log(
-        LogLevel.VERBOSE,
-        {
-          str1 = message
-          long1 = event.eventTime
-          long2 = event.downTime
-          int1 = event.action
-          int2 = event.classification
-          double1 = event.y.toDouble()
-        },
-        { "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2" })
-  }
+    fun logMotionEvent(event: MotionEvent, message: String) {
+        log(
+            LogLevel.VERBOSE,
+            {
+                str1 = message
+                long1 = event.eventTime
+                long2 = event.downTime
+                int1 = event.action
+                int2 = event.classification
+                double1 = event.y.toDouble()
+            },
+            {
+                "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2"
+            }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt
new file mode 100644
index 0000000..14882b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.shade
+
+/** A listener interface to be notified of expansion events for the quick settings panel. */
+fun interface ShadeQsExpansionListener {
+    /**
+     * Invoked whenever the quick settings expansion changes, when it is fully collapsed or expanded
+     */
+    fun onQsExpansionChanged(isQsExpanded: Boolean)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
new file mode 100644
index 0000000..09019a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.shade.data.repository
+
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.model.ShadeModel
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/** Business logic for shade interactions */
+@SysUISingleton
+class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) {
+
+    val shadeModel: Flow<ShadeModel> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : ShadeExpansionListener {
+                        override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
+                            // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field.
+                            // It is too noisy and produces extra events that consumers won't care
+                            // about
+                            val info =
+                                ShadeModel(
+                                    expansionAmount = event.fraction,
+                                    isExpanded = event.expanded,
+                                    isUserDragging = event.tracking
+                                )
+                            trySendWithFailureLogging(info, TAG, "updated shade expansion info")
+                        }
+                    }
+
+                shadeExpansionStateManager.addExpansionListener(callback)
+                trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info")
+
+                awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) }
+            }
+            .distinctUntilChanged()
+
+    companion object {
+        private const val TAG = "ShadeRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
new file mode 100644
index 0000000..ce0f4283
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.shade.domain.model
+
+import android.annotation.FloatRange
+
+/** Information about shade (NotificationPanel) expansion */
+data class ShadeModel(
+    /** 0 when collapsed, 1 when fully expanded. */
+    @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f,
+    /** Whether the panel should be considered expanded */
+    val isExpanded: Boolean = false,
+    /** Whether the user is actively dragging the panel. */
+    val isUserDragging: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index 7f7ff9cf..90c52bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.statusbar
 
 import android.app.PendingIntent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 9d4a27c..4ae0f6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -67,12 +67,15 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.GcUtils;
 import com.android.internal.view.AppearanceRegion;
+import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
 import com.android.systemui.statusbar.policy.CallbackController;
 import com.android.systemui.tracing.ProtoTracer;
 
+import java.io.FileDescriptor;
 import java.io.FileOutputStream;
+import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
@@ -182,6 +185,7 @@
     private int mLastUpdatedImeDisplayId = INVALID_DISPLAY;
     private ProtoTracer mProtoTracer;
     private final @Nullable CommandRegistry mRegistry;
+    private final @Nullable DumpHandler mDumpHandler;
 
     /**
      * These methods are called back on the main thread.
@@ -471,12 +475,18 @@
     }
 
     public CommandQueue(Context context) {
-        this(context, null, null);
+        this(context, null, null, null);
     }
 
-    public CommandQueue(Context context, ProtoTracer protoTracer, CommandRegistry registry) {
+    public CommandQueue(
+            Context context,
+            ProtoTracer protoTracer,
+            CommandRegistry registry,
+            DumpHandler dumpHandler
+    ) {
         mProtoTracer = protoTracer;
         mRegistry = registry;
+        mDumpHandler = dumpHandler;
         context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler);
         // We always have default display.
         setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE);
@@ -1175,6 +1185,35 @@
     }
 
     @Override
+    public void dumpProto(String[] args, ParcelFileDescriptor pfd) {
+        final FileDescriptor fd = pfd.getFileDescriptor();
+        // This is mimicking Binder#dumpAsync, but on this side of the binder. Might be possible
+        // to just throw this work onto the handler just like the other messages
+        Thread thr = new Thread("Sysui.dumpProto") {
+            public void run() {
+                try {
+                    if (mDumpHandler == null) {
+                        return;
+                    }
+                    // We won't be using the PrintWriter.
+                    OutputStream o = new OutputStream() {
+                        @Override
+                        public void write(int b) {}
+                    };
+                    mDumpHandler.dump(fd, new PrintWriter(o), args);
+                } finally {
+                    try {
+                        // Close the file descriptor so the TransferPipe finishes its thread
+                        pfd.close();
+                    } catch (Exception e) {
+                    }
+                }
+            }
+        };
+        thr.start();
+    }
+
+    @Override
     public void runGcForTest() {
         // Gc sysui
         GcUtils.runGcAndFinalizersSync();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 0c9e1ec..e21acb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -92,9 +92,6 @@
     /** Sets the state of whether the keyguard is fading away or not. */
     default void setKeyguardFadingAway(boolean keyguardFadingAway) {}
 
-    /** Sets the state of whether the quick settings is expanded or not. */
-    default void setQsExpanded(boolean expanded) {}
-
     /** Sets the state of whether the user activities are forced or not. */
     default void setForceUserActivity(boolean forceUserActivity) {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
index 8222c9d..c630feb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
@@ -68,6 +69,7 @@
     configurationController: ConfigurationController,
     private val statusBarStateController: StatusBarStateController,
     private val falsingManager: FalsingManager,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
     private val falsingCollector: FalsingCollector,
     dumpManager: DumpManager
@@ -126,6 +128,13 @@
                 initResources(context)
             }
         })
+
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            if (qsExpanded != isQsExpanded) {
+                qsExpanded = isQsExpanded
+            }
+        }
+
         mPowerManager = context.getSystemService(PowerManager::class.java)
         dumpManager.registerDumpable(this)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index ea7ec4f..450b757 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -71,9 +71,9 @@
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogLevel;
 import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
 import com.android.systemui.settings.CurrentUserTracker;
 import com.android.systemui.statusbar.policy.ConfigurationController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 11e3d17..f574be0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -29,6 +29,7 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.plugins.ActivityStarter;
@@ -181,8 +182,10 @@
     static CommandQueue provideCommandQueue(
             Context context,
             ProtoTracer protoTracer,
-            CommandRegistry registry) {
-        return new CommandQueue(context, protoTracer, registry);
+            CommandRegistry registry,
+            DumpHandler dumpHandler
+    ) {
+        return new CommandQueue(context, protoTracer, registry, dumpHandler);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index d88f07c..737b481 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -25,11 +25,12 @@
 import android.view.View
 import android.widget.FrameLayout
 import com.android.internal.annotations.GuardedBy
-import com.android.systemui.animation.Interpolators
 import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.StatusBarState.SHADE
 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
@@ -42,7 +43,6 @@
 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
 import com.android.systemui.util.leak.RotationUtils.Rotation
-
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -67,7 +67,8 @@
     private val stateController: StatusBarStateController,
     private val configurationController: ConfigurationController,
     private val contentInsetsProvider: StatusBarContentInsetsProvider,
-    private val animationScheduler: SystemStatusAnimationScheduler
+    private val animationScheduler: SystemStatusAnimationScheduler,
+    shadeExpansionStateManager: ShadeExpansionStateManager
 ) {
     private lateinit var tl: View
     private lateinit var tr: View
@@ -128,6 +129,13 @@
                 updateStatusBarState()
             }
         })
+
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            dlog("setQsExpanded $isQsExpanded")
+            synchronized(lock) {
+                nextViewState = nextViewState.copy(qsExpanded = isQsExpanded)
+            }
+        }
     }
 
     fun setUiExecutor(e: DelayableExecutor) {
@@ -138,13 +146,6 @@
         showingListener = l
     }
 
-    fun setQsExpanded(expanded: Boolean) {
-        dlog("setQsExpanded $expanded")
-        synchronized(lock) {
-            nextViewState = nextViewState.copy(qsExpanded = expanded)
-        }
-    }
-
     @UiThread
     fun setNewRotation(rot: Int) {
         dlog("updateRotation: $rot")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
index 17feaa8..9bdff92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.gesture
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.SwipeStatusBarAwayLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [SwipeStatusBarAwayGestureHandler]. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index dfba8cd..fc984618 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -119,6 +119,7 @@
                     regionSamplingEnabled,
                     updateFun
             )
+            initializeTextColors(regionSamplingInstance)
             regionSamplingInstance.startRegionSampler()
             regionSamplingInstances.put(v, regionSamplingInstance)
             connectSession()
@@ -362,18 +363,20 @@
         }
     }
 
+    private fun initializeTextColors(regionSamplingInstance: RegionSamplingInstance) {
+        val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper)
+        val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor)
+
+        val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI)
+        val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor)
+
+        regionSamplingInstance.setForegroundColors(lightColor, darkColor)
+    }
+
     private fun updateTextColorFromRegionSampler() {
         smartspaceViews.forEach {
-            val isRegionDark = regionSamplingInstances.getValue(it).currentRegionDarkness()
-            val themeID = if (isRegionDark.isDark) {
-                R.style.Theme_SystemUI
-            } else {
-                R.style.Theme_SystemUI_LightWallpaper
-            }
-            val themedContext = ContextThemeWrapper(context, themeID)
-            val wallpaperTextColor =
-                    Utils.getColorAttrDefaultColor(themedContext, R.attr.wallpaperTextColor)
-            it.setPrimaryTextColor(wallpaperTextColor)
+            val textColor = regionSamplingInstances.getValue(it).currentForegroundColor()
+            it.setPrimaryTextColor(textColor)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 7fbdd35..2734511 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -17,40 +17,27 @@
 package com.android.systemui.statusbar.notification
 
 import android.content.Context
-import android.util.Log
-import android.widget.Toast
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 class NotifPipelineFlags @Inject constructor(
     val context: Context,
     val featureFlags: FeatureFlags
 ) {
-    fun checkLegacyPipelineEnabled(): Boolean {
-        if (Compile.IS_DEBUG) {
-            Toast.makeText(context, "Old pipeline code running!", Toast.LENGTH_SHORT).show()
-        }
-        if (featureFlags.isEnabled(Flags.NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE)) {
-            throw RuntimeException("Old pipeline code running with new pipeline enabled")
-        } else {
-            Log.d("NotifPipeline", "Old pipeline code running with new pipeline enabled",
-                    Exception())
-        }
-        return false
-    }
-
     fun isDevLoggingEnabled(): Boolean =
         featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING)
 
-    fun isSmartspaceDedupingEnabled(): Boolean =
-            featureFlags.isEnabled(Flags.SMARTSPACE) &&
-                    featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING)
-
-    fun removeUnrankedNotifs(): Boolean =
-        featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS)
+    fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE)
 
     fun fullScreenIntentRequiresKeyguard(): Boolean =
         featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
+
+    val isStabilityIndexFixEnabled: Boolean by lazy {
+        featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
+    }
+
+    val isSemiStableSortEnabled: Boolean by lazy {
+        featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
index ad3dfed..3058fbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 7242506..d97b712 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -18,8 +18,10 @@
 
 import android.animation.ObjectAnimator
 import android.util.FloatProperty
+import com.android.systemui.Dumpable
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.shade.ShadeExpansionListener
@@ -32,17 +34,20 @@
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
+import java.io.PrintWriter
 import javax.inject.Inject
 import kotlin.math.min
 
 @SysUISingleton
 class NotificationWakeUpCoordinator @Inject constructor(
+    dumpManager: DumpManager,
     private val mHeadsUpManager: HeadsUpManager,
     private val statusBarStateController: StatusBarStateController,
     private val bypassController: KeyguardBypassController,
     private val dozeParameters: DozeParameters,
     private val screenOffAnimationController: ScreenOffAnimationController
-) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener {
+) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener,
+    Dumpable {
 
     private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>(
         "notificationVisibility") {
@@ -60,6 +65,7 @@
 
     private var mLinearDozeAmount: Float = 0.0f
     private var mDozeAmount: Float = 0.0f
+    private var mDozeAmountSource: String = "init"
     private var mNotificationVisibleAmount = 0.0f
     private var mNotificationsVisible = false
     private var mNotificationsVisibleForExpansion = false
@@ -142,6 +148,7 @@
         }
 
     init {
+        dumpManager.registerDumpable(this)
         mHeadsUpManager.addListener(this)
         statusBarStateController.addCallback(this)
         addListener(object : WakeUpListener {
@@ -248,13 +255,14 @@
             // Let's notify the scroller that an animation started
             notifyAnimationStart(mLinearDozeAmount == 1.0f)
         }
-        setDozeAmount(linear, eased)
+        setDozeAmount(linear, eased, source = "StatusBar")
     }
 
-    fun setDozeAmount(linear: Float, eased: Float) {
+    fun setDozeAmount(linear: Float, eased: Float, source: String) {
         val changed = linear != mLinearDozeAmount
         mLinearDozeAmount = linear
         mDozeAmount = eased
+        mDozeAmountSource = source
         mStackScrollerController.setDozeAmount(mDozeAmount)
         updateHideAmount()
         if (changed && linear == 0.0f) {
@@ -271,7 +279,7 @@
             // undefined state, so it's an indication that we should do state cleanup. We override
             // the doze amount to 0f (not dozing) so that the notifications are no longer hidden.
             // See: UnlockedScreenOffAnimationController.onFinishedWakingUp()
-            setDozeAmount(0f, 0f)
+            setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)")
         }
 
         if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) {
@@ -311,12 +319,11 @@
      */
     private fun overrideDozeAmountIfBypass(): Boolean {
         if (bypassController.bypassEnabled) {
-            var amount = 1.0f
-            if (statusBarStateController.state == StatusBarState.SHADE ||
-                statusBarStateController.state == StatusBarState.SHADE_LOCKED) {
-                amount = 0.0f
+            if (statusBarStateController.state == StatusBarState.KEYGUARD) {
+                setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)")
+            } else {
+                setDozeAmount(0f, 0f, source = "Override: bypass (shade)")
             }
-            setDozeAmount(amount, amount)
             return true
         }
         return false
@@ -332,7 +339,7 @@
      */
     private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean {
         if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) {
-            setDozeAmount(1f, 1f)
+            setDozeAmount(1f, 1f, source = "Override: animating screen off")
             return true
         }
 
@@ -414,6 +421,26 @@
     private fun shouldAnimateVisibility() =
             dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking
 
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("mLinearDozeAmount: $mLinearDozeAmount")
+        pw.println("mDozeAmount: $mDozeAmount")
+        pw.println("mDozeAmountSource: $mDozeAmountSource")
+        pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount")
+        pw.println("mNotificationsVisible: $mNotificationsVisible")
+        pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion")
+        pw.println("mVisibilityAmount: $mVisibilityAmount")
+        pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount")
+        pw.println("pulseExpanding: $pulseExpanding")
+        pw.println("state: ${StatusBarState.toString(state)}")
+        pw.println("fullyAwake: $fullyAwake")
+        pw.println("wakingUp: $wakingUp")
+        pw.println("willWakeUp: $willWakeUp")
+        pw.println("collapsedEnoughToHide: $collapsedEnoughToHide")
+        pw.println("pulsing: $pulsing")
+        pw.println("notificationsFullyHidden: $notificationsFullyHidden")
+        pw.println("canShowPulsingHuns: $canShowPulsingHuns")
+    }
+
     interface WakeUpListener {
         /**
          * Called whenever the notifications are fully hidden or shown
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index f8449ae..84ab0d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -68,6 +68,9 @@
      */
     var stableIndex: Int = -1
 
+    /** Access the index of the [section] or -1 if the entry does not have one */
+    val sectionIndex: Int get() = section?.index ?: -1
+
     /** Copies the state of another instance. */
     fun clone(other: ListAttachState) {
         parent = other.parent
@@ -95,11 +98,13 @@
      * This can happen if the entry is removed from a group that was broken up or if the entry was
      * filtered out during any of the filtering steps.
      */
-    fun detach() {
+    fun detach(includingStableIndex: Boolean) {
         parent = null
         section = null
         promoter = null
-        // stableIndex = -1  // TODO(b/241229236): Clear this once we fix the stability fragility
+        if (includingStableIndex) {
+            stableIndex = -1
+        }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 2887f97..df35c9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -602,7 +602,7 @@
 
         mInconsistencyTracker.logNewMissingNotifications(rankingMap);
         mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap);
-        if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) {
+        if (currentEntriesWithoutRankings != null) {
             for (NotificationEntry entry : currentEntriesWithoutRankings.values()) {
                 entry.mCancellationReason = REASON_UNKNOWN;
                 tryRemoveNotification(entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index e129ee4..3ae2545 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -54,6 +54,9 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder;
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper;
 import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
@@ -96,11 +99,14 @@
     // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated
     // TODO replace temp with collection pool for readability
     private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>();
+    private NotifPipelineFlags mFlags;
     private final boolean mAlwaysLogList;
 
     private List<ListEntry> mNotifList = new ArrayList<>();
     private List<ListEntry> mNewNotifList = new ArrayList<>();
 
+    private final SemiStableSort mSemiStableSort = new SemiStableSort();
+    private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank;
     private final PipelineState mPipelineState = new PipelineState();
     private final Map<String, GroupEntry> mGroups = new ArrayMap<>();
     private Collection<NotificationEntry> mAllEntries = Collections.emptyList();
@@ -141,6 +147,7 @@
     ) {
         mSystemClock = systemClock;
         mLogger = logger;
+        mFlags = flags;
         mAlwaysLogList = flags.isDevLoggingEnabled();
         mInteractionTracker = interactionTracker;
         mChoreographer = pipelineChoreographer;
@@ -527,7 +534,7 @@
             List<NotifFilter> filters) {
         Trace.beginSection("ShadeListBuilder.filterNotifs");
         final long now = mSystemClock.uptimeMillis();
-        for (ListEntry entry : entries)  {
+        for (ListEntry entry : entries) {
             if (entry instanceof GroupEntry) {
                 final GroupEntry groupEntry = (GroupEntry) entry;
 
@@ -958,7 +965,8 @@
      * filtered out during any of the filtering steps.
      */
     private void annulAddition(ListEntry entry) {
-        entry.getAttachState().detach();
+        // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility
+        entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled());
     }
 
     private void assignSections() {
@@ -978,7 +986,16 @@
 
     private void sortListAndGroups() {
         Trace.beginSection("ShadeListBuilder.sortListAndGroups");
-        // Assign sections to top-level elements and sort their children
+        if (mFlags.isSemiStableSortEnabled()) {
+            sortWithSemiStableSort();
+        } else {
+            sortWithLegacyStability();
+        }
+        Trace.endSection();
+    }
+
+    private void sortWithLegacyStability() {
+        // Sort all groups and the top level list
         for (ListEntry entry : mNotifList) {
             if (entry instanceof GroupEntry) {
                 GroupEntry parent = (GroupEntry) entry;
@@ -991,16 +1008,15 @@
         // Check for suppressed order changes
         if (!getStabilityManager().isEveryChangeAllowed()) {
             mForceReorderable = true;
-            boolean isSorted = isShadeSorted();
+            boolean isSorted = isShadeSortedLegacy();
             mForceReorderable = false;
             if (!isSorted) {
                 getStabilityManager().onEntryReorderSuppressed();
             }
         }
-        Trace.endSection();
     }
 
-    private boolean isShadeSorted() {
+    private boolean isShadeSortedLegacy() {
         if (!isSorted(mNotifList, mTopLevelComparator)) {
             return false;
         }
@@ -1014,6 +1030,43 @@
         return true;
     }
 
+    private void sortWithSemiStableSort() {
+        // Sort each group's children
+        boolean allSorted = true;
+        for (ListEntry entry : mNotifList) {
+            if (entry instanceof GroupEntry) {
+                GroupEntry parent = (GroupEntry) entry;
+                allSorted &= sortGroupChildren(parent.getRawChildren());
+            }
+        }
+        // Sort each section within the top level list
+        mNotifList.sort(mTopLevelComparator);
+        if (!getStabilityManager().isEveryChangeAllowed()) {
+            for (List<ListEntry> subList : getSectionSubLists(mNotifList)) {
+                allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList);
+            }
+            applyNewNotifList();
+        }
+        assignIndexes(mNotifList);
+        if (!allSorted) {
+            // Report suppressed order changes
+            getStabilityManager().onEntryReorderSuppressed();
+        }
+    }
+
+    private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) {
+        return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries);
+    }
+
+    private boolean sortGroupChildren(List<NotificationEntry> entries) {
+        if (getStabilityManager().isEveryChangeAllowed()) {
+            entries.sort(mGroupChildrenComparator);
+            return true;
+        } else {
+            return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator);
+        }
+    }
+
     /** Determine whether the items in the list are sorted according to the comparator */
     @VisibleForTesting
     public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) {
@@ -1036,27 +1089,41 @@
     /**
      * Assign the index of each notification relative to the total order
      */
-    private static void assignIndexes(List<ListEntry> notifList) {
+    private void assignIndexes(List<ListEntry> notifList) {
         if (notifList.size() == 0) return;
         NotifSection currentSection = requireNonNull(notifList.get(0).getSection());
         int sectionMemberIndex = 0;
         for (int i = 0; i < notifList.size(); i++) {
-            ListEntry entry = notifList.get(i);
+            final ListEntry entry = notifList.get(i);
             NotifSection section = requireNonNull(entry.getSection());
             if (section.getIndex() != currentSection.getIndex()) {
                 sectionMemberIndex = 0;
                 currentSection = section;
             }
-            entry.getAttachState().setStableIndex(sectionMemberIndex);
-            if (entry instanceof GroupEntry) {
-                GroupEntry parent = (GroupEntry) entry;
-                for (int j = 0; j < parent.getChildren().size(); j++) {
-                    entry = parent.getChildren().get(j);
-                    entry.getAttachState().setStableIndex(sectionMemberIndex);
-                    sectionMemberIndex++;
+            if (mFlags.isStabilityIndexFixEnabled()) {
+                entry.getAttachState().setStableIndex(sectionMemberIndex++);
+                if (entry instanceof GroupEntry) {
+                    final GroupEntry parent = (GroupEntry) entry;
+                    final NotificationEntry summary = parent.getSummary();
+                    if (summary != null) {
+                        summary.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
+                    for (NotificationEntry child : parent.getChildren()) {
+                        child.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
                 }
+            } else {
+                // This old implementation uses the same index number for the group as the first
+                // child, and fails to assign an index to the summary.  Remove once tested.
+                entry.getAttachState().setStableIndex(sectionMemberIndex);
+                if (entry instanceof GroupEntry) {
+                    final GroupEntry parent = (GroupEntry) entry;
+                    for (NotificationEntry child : parent.getChildren()) {
+                        child.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
+                }
+                sectionMemberIndex++;
             }
-            sectionMemberIndex++;
         }
     }
 
@@ -1196,7 +1263,7 @@
                 o2.getSectionIndex());
         if (cmp != 0) return cmp;
 
-        cmp = Integer.compare(
+        cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
                 getStableOrderIndex(o1),
                 getStableOrderIndex(o2));
         if (cmp != 0) return cmp;
@@ -1225,7 +1292,7 @@
 
 
     private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
-        int cmp = Integer.compare(
+        int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
                 getStableOrderIndex(o1),
                 getStableOrderIndex(o2));
         if (cmp != 0) return cmp;
@@ -1256,9 +1323,25 @@
             // let the stability manager constrain or allow reordering
             return -1;
         }
+        // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility
         return entry.getPreviousAttachState().getStableIndex();
     }
 
+    @Nullable
+    private Integer getStableOrderRank(ListEntry entry) {
+        if (getStabilityManager().isEntryReorderingAllowed(entry)) {
+            // let the stability manager constrain or allow reordering
+            return null;
+        }
+        if (entry.getAttachState().getSectionIndex()
+                != entry.getPreviousAttachState().getSectionIndex()) {
+            // stable index is only valid within the same section; otherwise we allow reordering
+            return null;
+        }
+        final int stableIndex = entry.getPreviousAttachState().getStableIndex();
+        return stableIndex == -1 ? null : stableIndex;
+    }
+
     private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
         final NotifFilter filter = findRejectingFilter(entry, now, filters);
         entry.getAttachState().setExcludingFilter(filter);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
index 211e374..68d1319 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coalescer
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 class GroupCoalescerLogger @Inject constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
index e8f352f..2919def 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.row.NotificationGuts
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8f3eb4f..8a31ed9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -18,6 +18,8 @@
 import android.app.Notification
 import android.app.Notification.GROUP_ALERT_SUMMARY
 import android.util.ArrayMap
+import android.util.ArraySet
+import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.NotificationRemoteInputManager
 import com.android.systemui.statusbar.notification.collection.GroupEntry
@@ -70,6 +72,7 @@
     @Main private val mExecutor: DelayableExecutor,
 ) : Coordinator {
     private val mEntriesBindingUntil = ArrayMap<String, Long>()
+    private val mEntriesUpdateTimes = ArrayMap<String, Long>()
     private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
     private lateinit var mNotifPipeline: NotifPipeline
     private var mNow: Long = -1
@@ -264,6 +267,9 @@
         }
         // After this method runs, all posted entries should have been handled (or skipped).
         mPostedEntries.clear()
+
+        // Also take this opportunity to clean up any stale entry update times
+        cleanUpEntryUpdateTimes()
     }
 
     /**
@@ -378,6 +384,9 @@
                 isAlerting = false,
                 isBinding = false,
             )
+
+            // Record the last updated time for this key
+            setUpdateTime(entry, mSystemClock.currentTimeMillis())
         }
 
         /**
@@ -419,6 +428,9 @@
                     cancelHeadsUpBind(posted.entry)
                 }
             }
+
+            // Update last updated time for this entry
+            setUpdateTime(entry, mSystemClock.currentTimeMillis())
         }
 
         /**
@@ -426,6 +438,7 @@
          */
         override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
             mPostedEntries.remove(entry.key)
+            mEntriesUpdateTimes.remove(entry.key)
             cancelHeadsUpBind(entry)
             val entryKey = entry.key
             if (mHeadsUpManager.isAlerting(entryKey)) {
@@ -454,7 +467,12 @@
             // never) in mPostedEntries to need to alert, we need to check every notification
             // known to the pipeline.
             for (entry in mNotifPipeline.allNotifs) {
-                // The only entries we can consider alerting for here are entries that have never
+                // Only consider entries that are recent enough, since we want to apply a fairly
+                // strict threshold for when an entry should be updated via only ranking and not an
+                // app-provided notification update.
+                if (!isNewEnoughForRankingUpdate(entry)) continue
+
+                // The only entries we consider alerting for here are entries that have never
                 // interrupted and that now say they should heads up; if they've alerted in the
                 // past, we don't want to incorrectly alert a second time if there wasn't an
                 // explicit notification update.
@@ -486,6 +504,41 @@
                 (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
     }
 
+    /**
+     * Sets the updated time for the given entry to the specified time.
+     */
+    @VisibleForTesting
+    fun setUpdateTime(entry: NotificationEntry, time: Long) {
+        mEntriesUpdateTimes[entry.key] = time
+    }
+
+    /**
+     * Checks whether the entry is new enough to be updated via ranking update.
+     * We want to avoid updating an entry too long after it was originally posted/updated when we're
+     * only reacting to a ranking change, as relevant ranking updates are expected to come in
+     * fairly soon after the posting of a notification.
+     */
+    private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean {
+        // If we don't have an update time for this key, default to "too old"
+        if (!mEntriesUpdateTimes.containsKey(entry.key)) return false
+
+        val updateTime = mEntriesUpdateTimes[entry.key] ?: return false
+        return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS
+    }
+
+    private fun cleanUpEntryUpdateTimes() {
+        // Because we won't update entries that are older than this amount of time anyway, clean
+        // up any entries that are too old to notify.
+        val toRemove = ArraySet<String>()
+        for ((key, updateTime) in mEntriesUpdateTimes) {
+            if (updateTime == null ||
+                    (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) {
+                toRemove.add(key)
+            }
+        }
+        mEntriesUpdateTimes.removeAll(toRemove)
+    }
+
     /** When an action is pressed on a notification, end HeadsUp lifetime extension. */
     private val mActionPressListener = Consumer<NotificationEntry> { entry ->
         if (mNotifsExtendingLifetime.contains(entry)) {
@@ -597,6 +650,9 @@
     companion object {
         private const val TAG = "HeadsUpCoordinator"
         private const val BIND_TIMEOUT = 1000L
+
+        // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord.
+        private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000
     }
 
     data class PostedEntry(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 8625cdb..dfaa291 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -1,9 +1,10 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import android.util.Log
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "HeadsUpCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 93146f9..d2db622 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -410,7 +410,7 @@
         // Only delay release if the summary is not inflated.
         // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been
         //  done by this point, we can revert back to checking for mInflatingNotifs.contains(...)
-        if (!isInflated(group.getSummary())) {
+        if (group.getSummary() != null && !isInflated(group.getSummary())) {
             mLogger.logDelayingGroupRelease(group, group.getSummary());
             return true;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
index c4f4ed5..9558f47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
index c687e1b..d804454 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "ShadeEventCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
new file mode 100644
index 0000000..9ec8e07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.notification.collection.listbuilder
+
+import androidx.annotation.VisibleForTesting
+import kotlin.math.sign
+
+class SemiStableSort {
+    val preallocatedWorkspace by lazy { ArrayList<Any>() }
+    val preallocatedAdditions by lazy { ArrayList<Any>() }
+    val preallocatedMapToIndex by lazy { HashMap<Any, Int>() }
+    val preallocatedMapToIndexComparator: Comparator<Any> by lazy {
+        Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 }
+    }
+
+    /**
+     * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+     * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+     * items will be combined to have the fewest elements out of order according to the [comparator]
+     * . The result will be placed into the original [items] list.
+     */
+    fun <T : Any> sort(
+        items: MutableList<T>,
+        stableOrder: StableOrder<in T>,
+        comparator: Comparator<in T>,
+    ): Boolean =
+        withWorkspace<T, Boolean> { workspace ->
+            val ordered =
+                sortTo(
+                    items,
+                    stableOrder,
+                    comparator,
+                    workspace,
+                )
+            items.clear()
+            items.addAll(workspace)
+            return ordered
+        }
+
+    /**
+     * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+     * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+     * items will be combined to have the fewest elements out of order according to the [comparator]
+     * . The result will be put into [output].
+     */
+    fun <T : Any> sortTo(
+        items: Iterable<T>,
+        stableOrder: StableOrder<in T>,
+        comparator: Comparator<in T>,
+        output: MutableList<T>,
+    ): Boolean {
+        if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}")
+        // If array already has elements, use subList to ensure we only append
+        val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+        items.filterTo(result) { stableOrder.getRank(it) != null }
+        result.sortBy { stableOrder.getRank(it)!! }
+        val isOrdered = result.isSorted(comparator)
+        withAdditions<T> { additions ->
+            items.filterTo(additions) { stableOrder.getRank(it) == null }
+            additions.sortWith(comparator)
+            insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+        }
+        return isOrdered
+    }
+
+    /**
+     * Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the
+     * result in [output]. Items with a [stableOrder] will be in that order, items without a
+     * [stableOrder] will remain in same relative order as the input, and the two sets of items will
+     * be combined to have the fewest elements moved from their locations in the original.
+     */
+    fun <T : Any> stabilizeTo(
+        sortedItems: Iterable<T>,
+        stableOrder: StableOrder<in T>,
+        output: MutableList<T>,
+    ): Boolean {
+        // Append to the output array if present
+        val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+        sortedItems.filterTo(result) { stableOrder.getRank(it) != null }
+        val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! }
+        val isOrdered = result.isSorted(stableRankComparator)
+        if (!isOrdered) {
+            result.sortWith(stableRankComparator)
+        }
+        if (result.isEmpty()) {
+            sortedItems.filterTo(result) { stableOrder.getRank(it) == null }
+            return isOrdered
+        }
+        withAdditions<T> { additions ->
+            sortedItems.filterTo(additions) { stableOrder.getRank(it) == null }
+            if (additions.isNotEmpty()) {
+                withIndexOfComparator(sortedItems) { comparator ->
+                    insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+                }
+            }
+        }
+        return isOrdered
+    }
+
+    private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R {
+        preallocatedWorkspace.clear()
+        val result = block(preallocatedWorkspace as ArrayList<T>)
+        preallocatedWorkspace.clear()
+        return result
+    }
+
+    private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) {
+        preallocatedAdditions.clear()
+        block(preallocatedAdditions as ArrayList<T>)
+        preallocatedAdditions.clear()
+    }
+
+    private inline fun <T : Any> withIndexOfComparator(
+        sortedItems: Iterable<T>,
+        block: (Comparator<in T>) -> Unit
+    ) {
+        preallocatedMapToIndex.clear()
+        sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i }
+        block(preallocatedMapToIndexComparator as Comparator<in T>)
+        preallocatedMapToIndex.clear()
+    }
+
+    companion object {
+
+        /**
+         * This is the core of the algorithm.
+         *
+         * Insert [preSortedAdditions] (the elements to be inserted) into [existing] without
+         * changing the relative order of any elements already in [existing], even though those
+         * elements may be mis-ordered relative to the [comparator], such that the total number of
+         * elements which are ordered incorrectly according to the [comparator] is fewest.
+         */
+        private fun <T> insertPreSortedElementsWithFewestMisOrderings(
+            existing: MutableList<T>,
+            preSortedAdditions: Iterable<T>,
+            comparator: Comparator<in T>,
+        ) {
+            if (DEBUG) println("  To $existing insert $preSortedAdditions with fewest misordering")
+            var iStart = 0
+            preSortedAdditions.forEach { toAdd ->
+                if (DEBUG) println("    need to add $toAdd to $existing, starting at $iStart")
+                var cmpSum = 0
+                var cmpSumMax = 0
+                var iCmpSumMax = iStart
+                if (DEBUG) print("      ")
+                for (i in iCmpSumMax until existing.size) {
+                    val cmp = comparator.compare(toAdd, existing[i]).sign
+                    cmpSum += cmp
+                    if (cmpSum > cmpSumMax) {
+                        cmpSumMax = cmpSum
+                        iCmpSumMax = i + 1
+                    }
+                    if (DEBUG) print("sum[$i]=$cmpSum, ")
+                }
+                if (DEBUG) println("inserting $toAdd at $iCmpSumMax")
+                existing.add(iCmpSumMax, toAdd)
+                iStart = iCmpSumMax + 1
+            }
+        }
+
+        /** Determines if a list is correctly sorted according to the given comparator */
+        @VisibleForTesting
+        fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean {
+            if (this.size <= 1) {
+                return true
+            }
+            val iterator = this.iterator()
+            var previous = iterator.next()
+            var current: T?
+            while (iterator.hasNext()) {
+                current = iterator.next()
+                if (comparator.compare(previous, current) > 0) {
+                    return false
+                }
+                previous = current
+            }
+            return true
+        }
+    }
+
+    fun interface StableOrder<T> {
+        fun getRank(item: T): Int?
+    }
+}
+
+val DEBUG = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
new file mode 100644
index 0000000..d8f75f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
@@ -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 com.android.systemui.statusbar.notification.collection.listbuilder
+
+import com.android.systemui.statusbar.notification.collection.ListEntry
+
+object ShadeListBuilderHelper {
+    fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> =
+        getContiguousSubLists(entries, minLength = 1) { it.sectionIndex }
+
+    inline fun <T : Any, K : Any> getContiguousSubLists(
+        itemList: List<T>,
+        minLength: Int = 1,
+        key: (T) -> K,
+    ): Iterable<List<T>> {
+        val subLists = mutableListOf<List<T>>()
+        val numEntries = itemList.size
+        var currentSectionStartIndex = 0
+        var currentSectionKey: K? = null
+        for (i in 0 until numEntries) {
+            val sectionKey = key(itemList[i])
+            if (currentSectionKey == null) {
+                currentSectionKey = sectionKey
+            } else if (currentSectionKey != sectionKey) {
+                val length = i - currentSectionStartIndex
+                if (length >= minLength) {
+                    subLists.add(itemList.subList(currentSectionStartIndex, i))
+                }
+                currentSectionStartIndex = i
+                currentSectionKey = sectionKey
+            }
+        }
+        val length = numEntries - currentSectionStartIndex
+        if (length >= minLength) {
+            subLists.add(itemList.subList(currentSectionStartIndex, numEntries))
+        }
+        return subLists
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index d8dae5d..8e052c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.notification.collection.listbuilder
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index aa27e1e..911a2d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -20,13 +20,13 @@
 import android.service.notification.NotificationListenerService
 import android.service.notification.NotificationListenerService.RankingMap
 import android.service.notification.StatusBarNotification
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.LogLevel.WTF
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.LogLevel.WTF
 import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason
 import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
index 38e3d49..9c71e5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.util.Compile
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
index 6d1071c..b4b9438 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import java.lang.RuntimeException
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
index 5dbec8d..d4f11fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.interruption
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 99d320d..073b6b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.notification.interruption
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotificationInterruptLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index c5a6921..c4f5a3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.notification.interruption;
 
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD;
+import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR;
 
 import android.app.NotificationManager;
 import android.content.ContentResolver;
@@ -32,6 +34,8 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -68,10 +72,30 @@
     private final NotificationInterruptLogger mLogger;
     private final NotifPipelineFlags mFlags;
     private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+    private final UiEventLogger mUiEventLogger;
 
     @VisibleForTesting
     protected boolean mUseHeadsUp = false;
 
+    public enum NotificationInterruptEvent implements UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "FSI suppressed for suppressive GroupAlertBehavior")
+        FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235),
+
+        @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard")
+        FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236);
+
+        private final int mId;
+
+        NotificationInterruptEvent(int id) {
+            mId = id;
+        }
+
+        @Override
+        public int getId() {
+            return mId;
+        }
+    }
+
     @Inject
     public NotificationInterruptStateProviderImpl(
             ContentResolver contentResolver,
@@ -85,7 +109,8 @@
             NotificationInterruptLogger logger,
             @Main Handler mainHandler,
             NotifPipelineFlags flags,
-            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+            UiEventLogger uiEventLogger) {
         mContentResolver = contentResolver;
         mPowerManager = powerManager;
         mDreamManager = dreamManager;
@@ -97,6 +122,7 @@
         mLogger = logger;
         mFlags = flags;
         mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
+        mUiEventLogger = uiEventLogger;
         ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -203,7 +229,9 @@
             // b/231322873: Detect and report an event when a notification has both an FSI and a
             // suppressive groupAlertBehavior, and now correctly block the FSI from firing.
             final int uid = entry.getSbn().getUid();
+            final String packageName = entry.getSbn().getPackageName();
             android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "groupAlertBehavior");
+            mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid, packageName);
             mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
             return false;
         }
@@ -249,7 +277,9 @@
             // Detect the case determined by b/231322873 to launch FSI while device is in use,
             // as blocked by the correct implementation, and report the event.
             final int uid = entry.getSbn().getUid();
+            final String packageName = entry.getSbn().getPackageName();
             android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "no hun or keyguard");
+            mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName);
             mLogger.logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
             return false;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
index fe03b2a..10197a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.logging
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationRenderLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.stack.NotificationSection
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
index ab91926..46fef3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.row
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
index f9923b2..8a5d29a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.row
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 2719dd8..b2628e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -142,6 +142,11 @@
      */
     private boolean mIsFlingRequiredAfterLockScreenSwipeUp = false;
 
+    /**
+     * Whether the shade is currently closing.
+     */
+    private boolean mIsClosing;
+
     @VisibleForTesting
     public boolean isFlingRequiredAfterLockScreenSwipeUp() {
         return mIsFlingRequiredAfterLockScreenSwipeUp;
@@ -717,6 +722,20 @@
                 && mStatusBarKeyguardViewManager.isBouncerInTransit();
     }
 
+    /**
+     * @param isClosing Whether the shade is currently closing.
+     */
+    public void setIsClosing(boolean isClosing) {
+        mIsClosing = isClosing;
+    }
+
+    /**
+     * @return Whether the shade is currently closing.
+     */
+    public boolean isClosing() {
+        return mIsClosing;
+    }
+
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println("mTopPadding=" + mTopPadding);
@@ -761,5 +780,6 @@
                 + mIsFlingRequiredAfterLockScreenSwipeUp);
         pw.println("mZDistanceBetweenElements=" + mZDistanceBetweenElements);
         pw.println("mBaseZHeight=" + mBaseZHeight);
+        pw.println("mIsClosing=" + mIsClosing);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
index cb7dfe8..b61c55e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.statusbar.notification.stack
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationSectionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "NotifSections"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 55c577f..2272411 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -255,7 +255,6 @@
     private boolean mClearAllInProgress;
     private FooterClearAllListener mFooterClearAllListener;
     private boolean mFlingAfterUpEvent;
-
     /**
      * Was the scroller scrolled to the top when the down motion was observed?
      */
@@ -4020,8 +4019,9 @@
         setOwnScrollY(0);
     }
 
+    @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private void setIsExpanded(boolean isExpanded) {
+    void setIsExpanded(boolean isExpanded) {
         boolean changed = isExpanded != mIsExpanded;
         mIsExpanded = isExpanded;
         mStackScrollAlgorithm.setIsExpanded(isExpanded);
@@ -4842,13 +4842,21 @@
         }
     }
 
+    @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private void setOwnScrollY(int ownScrollY) {
+    void setOwnScrollY(int ownScrollY) {
         setOwnScrollY(ownScrollY, false /* animateScrollChangeListener */);
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) {
+        // Avoid Flicking during clear all
+        // when the shade finishes closing, onExpansionStopped will call
+        // resetScrollPosition to setOwnScrollY to 0
+        if (mAmbientState.isClosing()) {
+            return;
+        }
+
         if (ownScrollY != mOwnScrollY) {
             // We still want to call the normal scrolled changed for accessibility reasons
             onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 5f79c0e..4c52db7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.stack
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
index cb4a088..f5de678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.stack
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 25fd483..70cf56d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -262,8 +262,6 @@
     @Override
     void startActivity(Intent intent, boolean dismissShade, Callback callback);
 
-    void setQsExpanded(boolean expanded);
-
     boolean isWakeUpComingFromTouch();
 
     boolean isFalsingThresholdNeeded();
@@ -455,6 +453,9 @@
 
     void collapseShade();
 
+    /** Collapse the shade, but conditional on a flag specific to the trigger of a bugreport. */
+    void collapseShadeForBugreport();
+
     int getWakefulnessState();
 
     boolean isScreenFullyOff();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 2c834cf..29642be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -868,6 +868,11 @@
             mBubblesOptional.get().setExpandListener(mBubbleExpandListener);
         }
 
+        // Do not restart System UI when the bugreport flag changes.
+        mFeatureFlags.addListener(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, event -> {
+            event.requestNoRestart();
+        });
+
         mStatusBarSignalPolicy.init();
         mKeyguardIndicationController.init();
 
@@ -1772,18 +1777,6 @@
     }
 
     @Override
-    public void setQsExpanded(boolean expanded) {
-        mNotificationShadeWindowController.setQsExpanded(expanded);
-        mNotificationPanelViewController.setStatusAccessibilityImportance(expanded
-                ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-        mNotificationPanelViewController.updateSystemUiStateFlags();
-        if (getNavigationBarView() != null) {
-            getNavigationBarView().onStatusBarPanelStateChanged();
-        }
-    }
-
-    @Override
     public boolean isWakeUpComingFromTouch() {
         return mWakeUpComingFromTouch;
     }
@@ -3561,6 +3554,13 @@
         }
     }
 
+    @Override
+    public void collapseShadeForBugreport() {
+        if (!mFeatureFlags.isEnabled(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT)) {
+            collapseShade();
+        }
+    }
+
     @VisibleForTesting
     final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
index b987f68..b965ac9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm
@@ -95,14 +96,7 @@
     var bouncerShowing: Boolean = false
     var altBouncerShowing: Boolean = false
     var launchingAffordance: Boolean = false
-    var qSExpanded = false
-        set(value) {
-            val changed = field != value
-            field = value
-            if (changed && !value) {
-                maybePerformPendingUnlock()
-            }
-        }
+    var qsExpanded = false
 
     @Inject
     constructor(
@@ -111,6 +105,7 @@
         statusBarStateController: StatusBarStateController,
         lockscreenUserManager: NotificationLockscreenUserManager,
         keyguardStateController: KeyguardStateController,
+        shadeExpansionStateManager: ShadeExpansionStateManager,
         dumpManager: DumpManager
     ) {
         this.mKeyguardStateController = keyguardStateController
@@ -132,6 +127,14 @@
             }
         })
 
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            val changed = qsExpanded != isQsExpanded
+            qsExpanded = isQsExpanded
+            if (changed && !isQsExpanded) {
+                maybePerformPendingUnlock()
+            }
+        }
+
         val dismissByDefault = if (context.resources.getBoolean(
                         com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0
         tunerService.addTunable(object : TunerService.Tunable {
@@ -160,7 +163,7 @@
     ): Boolean {
         if (biometricSourceType == BiometricSourceType.FACE && bypassEnabled) {
             val can = canBypass()
-            if (!can && (isPulseExpanding || qSExpanded)) {
+            if (!can && (isPulseExpanding || qsExpanded)) {
                 pendingUnlock = PendingUnlock(biometricSourceType, isStrongBiometric)
             }
             return can
@@ -189,7 +192,7 @@
                 altBouncerShowing -> true
                 statusBarStateController.state != StatusBarState.KEYGUARD -> false
                 launchingAffordance -> false
-                isPulseExpanding || qSExpanded -> false
+                isPulseExpanding || qsExpanded -> false
                 else -> true
             }
         }
@@ -214,7 +217,7 @@
         pw.println("  altBouncerShowing: $altBouncerShowing")
         pw.println("  isPulseExpanding: $isPulseExpanding")
         pw.println("  launchingAffordance: $launchingAffordance")
-        pw.println("  qSExpanded: $qSExpanded")
+        pw.println("  qSExpanded: $qsExpanded")
         pw.println("  hasFaceFeature: $hasFaceFeature")
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
index 02b2354..4839fe6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
@@ -19,9 +19,9 @@
 import android.util.DisplayMetrics
 import android.view.View
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.LSShadeTransitionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
index 00c3e8f..5e2a7c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
@@ -26,6 +26,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
@@ -67,7 +68,7 @@
                         ActivityLaunchAnimator.Controller.fromView(v, null),
                         true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM);
             } else {
-                mUserSwitchDialogController.showDialog(v);
+                mUserSwitchDialogController.showDialog(v.getContext(), Expandable.fromView(v));
             }
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index ece7ee0..86f6ff8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -372,7 +372,7 @@
             mIconSize = mContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.status_bar_icon_size);
 
-            if (statusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (statusBarPipelineFlags.useNewMobileIcons()) {
                 // This starts the flow for the new pipeline, and will notify us of changes
                 mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel();
                 MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
@@ -451,7 +451,7 @@
         @VisibleForTesting
         protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
             final BaseStatusBarFrameLayout view;
-            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (mStatusBarPipelineFlags.useNewWifiIcon()) {
                 view = onCreateModernStatusBarWifiView(slot);
                 // When [ModernStatusBarWifiView] is created, it will automatically apply the
                 // correct view state so we don't need to call applyWifiState.
@@ -474,9 +474,9 @@
                 String slot,
                 MobileIconState state
         ) {
-            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (mStatusBarPipelineFlags.useNewMobileIcons()) {
                 throw new IllegalStateException("Attempting to add a mobile icon while the new "
-                        + "pipeline is enabled is not supported");
+                        + "icons are enabled is not supported");
             }
 
             // Use the `subId` field as a key to query for the correct context
@@ -497,7 +497,7 @@
                 String slot,
                 int subId
         ) {
-            if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
                 throw new IllegalStateException("Attempting to add a mobile icon using the new"
                         + "pipeline, but the enabled flag is false.");
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index e106b9e..31e960a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -224,9 +224,9 @@
      */
     @Override
     public void setMobileIcons(String slot, List<MobileIconState> iconStates) {
-        if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+        if (mStatusBarPipelineFlags.useNewMobileIcons()) {
             Log.d(TAG, "ignoring old pipeline callbacks, because the new "
-                    + "pipeline frontend is enabled");
+                    + "icons are enabled");
             return;
         }
         Slot mobileSlot = mStatusBarIconList.getSlot(slot);
@@ -249,9 +249,9 @@
 
     @Override
     public void setNewMobileIconSubIds(List<Integer> subIds) {
-        if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+        if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
             Log.d(TAG, "ignoring new pipeline callback, "
-                    + "since the frontend is disabled");
+                    + "since the new icons are disabled");
             return;
         }
         Slot mobileSlot = mStatusBarIconList.getSlot("mobile");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 5f5ec68..5480f5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -60,7 +60,6 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.data.BouncerView;
-import com.android.systemui.keyguard.data.BouncerViewDelegate;
 import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
 import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
 import com.android.systemui.navigationbar.NavigationBarView;
@@ -136,7 +135,7 @@
     private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController;
     private final BouncerCallbackInteractor mBouncerCallbackInteractor;
     private final BouncerInteractor mBouncerInteractor;
-    private final BouncerViewDelegate mBouncerViewDelegate;
+    private final BouncerView mBouncerView;
     private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
 
     private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() {
@@ -327,7 +326,7 @@
         mKeyguardSecurityModel = keyguardSecurityModel;
         mBouncerCallbackInteractor = bouncerCallbackInteractor;
         mBouncerInteractor = bouncerInteractor;
-        mBouncerViewDelegate = bouncerView.getDelegate();
+        mBouncerView = bouncerView;
         mFoldAodAnimationController = sysUIUnfoldComponent
                 .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
         mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
@@ -804,7 +803,7 @@
     private void setDozing(boolean dozing) {
         if (mDozing != dozing) {
             mDozing = dozing;
-            if (dozing || mBouncer.needsFullscreenBouncer()
+            if (dozing || needsFullscreenBouncer()
                     || mKeyguardStateController.isOccluded()) {
                 reset(dozing /* hideBouncerWhenShowing */);
             }
@@ -1081,7 +1080,7 @@
      * @return whether a back press can be handled right now.
      */
     public boolean canHandleBackPressed() {
-        return mBouncer.isShowing();
+        return bouncerIsShowing();
     }
 
     /**
@@ -1094,7 +1093,7 @@
 
         mCentralSurfaces.endAffordanceLaunch();
         // The second condition is for SIM card locked bouncer
-        if (bouncerIsScrimmed() && needsFullscreenBouncer()) {
+        if (bouncerIsScrimmed() && !needsFullscreenBouncer()) {
             hideBouncer(false);
             updateStates();
         } else {
@@ -1124,8 +1123,8 @@
     }
 
     public boolean isFullscreenBouncer() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.isFullScreenBouncer();
+        if (mBouncerView.getDelegate() != null) {
+            return mBouncerView.getDelegate().isFullScreenBouncer();
         }
         return mBouncer != null && mBouncer.isFullscreenBouncer();
     }
@@ -1284,15 +1283,15 @@
     }
 
     public boolean shouldDismissOnMenuPressed() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.shouldDismissOnMenuPressed();
+        if (mBouncerView.getDelegate() != null) {
+            return mBouncerView.getDelegate().shouldDismissOnMenuPressed();
         }
         return mBouncer != null && mBouncer.shouldDismissOnMenuPressed();
     }
 
     public boolean interceptMediaKey(KeyEvent event) {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.interceptMediaKey(event);
+        if (mBouncerView.getDelegate() != null) {
+            return mBouncerView.getDelegate().interceptMediaKey(event);
         }
         return mBouncer != null && mBouncer.interceptMediaKey(event);
     }
@@ -1301,8 +1300,8 @@
      * @return true if the pre IME back event should be handled
      */
     public boolean dispatchBackKeyEventPreIme() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.dispatchBackKeyEventPreIme();
+        if (mBouncerView.getDelegate() != null) {
+            return mBouncerView.getDelegate().dispatchBackKeyEventPreIme();
         }
         return mBouncer != null && mBouncer.dispatchBackKeyEventPreIme();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
index b9a1413..81edff4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.statusbar.phone
 
 import android.app.PendingIntent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
index 28ed080..d64bc58 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.phone.fragment
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.CollapsedSbFragmentLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
index 0d52f46..e498ae4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
@@ -19,6 +19,7 @@
 import android.content.Intent
 import android.os.UserHandle
 import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.ActivityStarter
@@ -75,7 +76,7 @@
                         null /* ActivityLaunchAnimator.Controller */,
                         true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM)
             } else {
-                userSwitcherDialogController.showDialog(view)
+                userSwitcherDialogController.showDialog(view.context, Expandable.fromView(view))
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
index 9b8b643..06cd12d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -24,29 +24,19 @@
 /** All flagging methods related to the new status bar pipeline (see b/238425913). */
 @SysUISingleton
 class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) {
-    /**
-     * Returns true if we should run the new pipeline backend.
-     *
-     * The new pipeline backend hooks up to all our external callbacks, logs those callback inputs,
-     * and logs the output state.
-     */
-    fun isNewPipelineBackendEnabled(): Boolean =
-        featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_BACKEND)
+    /** True if we should display the mobile icons using the new status bar data pipeline. */
+    fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS)
+
+    /** True if we should display the wifi icon using the new status bar data pipeline. */
+    fun useNewWifiIcon(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON)
+
+    // TODO(b/238425913): Add flags to only run the mobile backend or wifi backend so we get the
+    //   logging without getting the UI effects.
 
     /**
-     * Returns true if we should run the new pipeline frontend *and* backend.
-     *
-     * The new pipeline frontend will use the outputted state from the new backend and will make the
-     * correct changes to the UI.
-     */
-    fun isNewPipelineFrontendEnabled(): Boolean =
-        isNewPipelineBackendEnabled() &&
-            featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_FRONTEND)
-
-    /**
-     * Returns true if we should apply some coloring to icons that were rendered with the new
+     * Returns true if we should apply some coloring to the wifi icon that was rendered with the new
      * pipeline to help with debugging.
      */
-    // For now, just always apply the debug coloring if we've enabled frontend rendering.
-    fun useNewPipelineDebugColoring(): Boolean = isNewPipelineFrontendEnabled()
+    // For now, just always apply the debug coloring if we've enabled the new icon.
+    fun useWifiDebugColoring(): Boolean = useNewWifiIcon()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index dbb1aa5..d3cf32f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -18,10 +18,10 @@
 
 import android.net.Network
 import android.net.NetworkCapabilities
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 681cf72..93448c1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -39,7 +39,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import java.util.concurrent.Executor
@@ -64,6 +63,9 @@
     /** Observable for the current wifi enabled status. */
     val isWifiEnabled: StateFlow<Boolean>
 
+    /** Observable for the current wifi default status. */
+    val isWifiDefault: StateFlow<Boolean>
+
     /** Observable for the current wifi network. */
     val wifiNetwork: StateFlow<WifiNetworkModel>
 
@@ -103,7 +105,7 @@
             merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
                 .mapLatest { wifiManager.isWifiEnabled }
                 .distinctUntilChanged()
-                .logOutputChange(logger, "enabled")
+                .logInputChange(logger, "enabled")
                 .stateIn(
                     scope = scope,
                     started = SharingStarted.WhileSubscribed(),
@@ -111,6 +113,39 @@
                 )
         }
 
+    override val isWifiDefault: StateFlow<Boolean> = conflatedCallbackFlow {
+        // Note: This callback doesn't do any logging because we already log every network change
+        // in the [wifiNetwork] callback.
+        val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+            override fun onCapabilitiesChanged(
+                network: Network,
+                networkCapabilities: NetworkCapabilities
+            ) {
+                // This method will always be called immediately after the network becomes the
+                // default, in addition to any time the capabilities change while the network is
+                // the default.
+                // If this network contains valid wifi info, then wifi is the default network.
+                val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
+                trySend(wifiInfo != null)
+            }
+
+            override fun onLost(network: Network) {
+                // The system no longer has a default network, so wifi is definitely not default.
+                trySend(false)
+            }
+        }
+
+        connectivityManager.registerDefaultNetworkCallback(callback)
+        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+    }
+        .distinctUntilChanged()
+        .logInputChange(logger, "isWifiDefault")
+        .stateIn(
+            scope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = false
+        )
+
     override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow {
         var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 04b17ed..3a3e611 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -59,6 +59,9 @@
     /** Our current enabled status. */
     val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled
 
+    /** Our current default status. */
+    val isDefault: Flow<Boolean> = wifiRepository.isWifiDefault
+
     /** Our current wifi network. See [WifiNetworkModel]. */
     val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
index e23f8c7..cc6a375 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
@@ -48,7 +48,7 @@
     /** The color that should be used to tint the icon. */
     val tint: Flow<Int> =
         flowOf(
-            if (statusBarPipelineFlags.useNewPipelineDebugColoring()) {
+            if (statusBarPipelineFlags.useWifiDebugColoring()) {
                 debugTint
             } else {
                 DEFAULT_TINT
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index ebbd77b..160c577 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -124,9 +124,10 @@
     private val wifiIcon: StateFlow<Icon.Resource?> =
         combine(
             interactor.isEnabled,
+            interactor.isDefault,
             interactor.isForceHidden,
             interactor.wifiNetwork,
-        ) { isEnabled, isForceHidden, wifiNetwork ->
+        ) { isEnabled, isDefault, isForceHidden, wifiNetwork ->
             if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
                 return@combine null
             }
@@ -135,6 +136,7 @@
             val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription())
 
             return@combine when {
+                isDefault -> icon
                 wifiConstants.alwaysShowIconIfEnabled -> icon
                 !connectivityConstants.hasDataCapabilities -> icon
                 wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 2f0ebf7..28a9b97 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -43,11 +43,7 @@
     }
 
     override fun getCount(): Int {
-        return if (controller.isKeyguardShowing) {
-            users.count { !it.isRestricted }
-        } else {
-            users.size
-        }
+        return users.size
     }
 
     override fun getItem(position: Int): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index d7c81af..df1e80b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.statusbar.policy
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index dc73d1f..f63d652 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
@@ -36,6 +36,7 @@
 import com.android.keyguard.dagger.KeyguardUserSwitcherScope;
 import com.android.settingslib.drawable.CircleFramedDrawable;
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -190,7 +191,8 @@
             mUiEventLogger.log(
                     LockscreenGestureLogger.LockscreenUiEvent.LOCKSCREEN_SWITCH_USER_TAP);
 
-            mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground);
+            mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground.getContext(),
+                    Expandable.fromView(mUserAvatarViewWithBackground));
         });
 
         mUserAvatarView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index 606a11a..a7185cb 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.temporarydisplay
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 
 /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
 open class TemporaryViewLogger(
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 1a25e4d..1a8aafb 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.graphics.Rect
-import android.media.MediaRoute2Info
 import android.os.PowerManager
 import android.view.Gravity
 import android.view.MotionEvent
@@ -27,25 +26,25 @@
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.widget.TextView
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.internal.widget.CachingIconView
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.ViewHierarchyAnimator
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.common.ui.binder.TextViewBinder
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
-import com.android.systemui.media.taptotransfer.sender.ChipStateSender
 import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger
-import com.android.systemui.media.taptotransfer.sender.TransferStatus
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
 import javax.inject.Inject
@@ -78,11 +77,11 @@
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
         powerManager: PowerManager,
-        private val uiEventLogger: MediaTttSenderUiEventLogger,
         private val falsingManager: FalsingManager,
         private val falsingCollector: FalsingCollector,
         private val viewUtil: ViewUtil,
-) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
+        private val vibratorHelper: VibratorHelper,
+) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>(
         context,
         logger,
         windowManager,
@@ -104,15 +103,13 @@
     override fun start() {}
 
     override fun updateView(
-        newInfo: ChipSenderInfo,
+        newInfo: ChipbarInfo,
         currentView: ViewGroup
     ) {
         // TODO(b/245610654): Adding logging here.
 
-        val chipState = newInfo.state
-
         // Detect falsing touches on the chip.
-        parent = currentView.requireViewById(R.id.media_ttt_sender_chip)
+        parent = currentView.requireViewById(R.id.chipbar_root_view)
         parent.touchHandler = object : Gefingerpoken {
             override fun onTouchEvent(ev: MotionEvent?): Boolean {
                 falsingCollector.onTouchEvent(ev)
@@ -120,47 +117,54 @@
             }
         }
 
-        // App icon
-        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
-            context, newInfo.routeInfo.clientPackageName, logger
-        )
-        val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
-        iconView.setImageDrawable(iconInfo.drawable)
-        iconView.contentDescription = iconInfo.contentDescription
+        // ---- Start icon ----
+        val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon)
+        IconViewBinder.bind(newInfo.startIcon, iconView)
 
-        // Text
-        val otherDeviceName = newInfo.routeInfo.name.toString()
-        val chipText = chipState.getChipTextString(context, otherDeviceName)
-        currentView.requireViewById<TextView>(R.id.text).text = chipText
+        // ---- Text ----
+        val textView = currentView.requireViewById<TextView>(R.id.text)
+        TextViewBinder.bind(textView, newInfo.text)
 
+        // ---- End item ----
         // Loading
         currentView.requireViewById<View>(R.id.loading).visibility =
-            (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue()
+            (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue()
 
-        // Undo
-        val undoView = currentView.requireViewById<View>(R.id.undo)
-        val undoClickListener = chipState.undoClickListener(
-                this,
-                newInfo.routeInfo,
-                newInfo.undoCallback,
-                uiEventLogger,
-                falsingManager,
-        )
-        undoView.setOnClickListener(undoClickListener)
-        undoView.visibility = (undoClickListener != null).visibleIfTrue()
+        // Error
+        currentView.requireViewById<View>(R.id.error).visibility =
+            (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue()
 
-        // Failure
-        currentView.requireViewById<View>(R.id.failure_icon).visibility =
-            (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue()
+        // Button
+        val buttonView = currentView.requireViewById<TextView>(R.id.end_button)
+        if (newInfo.endItem is ChipbarEndItem.Button) {
+            TextViewBinder.bind(buttonView, newInfo.endItem.text)
 
-        // For accessibility
+            val onClickListener = View.OnClickListener { clickedView ->
+                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+                newInfo.endItem.onClickListener.onClick(clickedView)
+            }
+
+            buttonView.setOnClickListener(onClickListener)
+            buttonView.visibility = View.VISIBLE
+        } else {
+            buttonView.visibility = View.GONE
+        }
+
+        // ---- Overall accessibility ----
         currentView.requireViewById<ViewGroup>(
-                R.id.media_ttt_sender_chip_inner
-        ).contentDescription = "${iconInfo.contentDescription} $chipText"
+                R.id.chipbar_inner
+        ).contentDescription =
+            "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " +
+                "${newInfo.text.loadText(context)}"
+
+        // ---- Haptics ----
+        newInfo.vibrationEffect?.let {
+            vibratorHelper.vibrate(it)
+        }
     }
 
     override fun animateViewIn(view: ViewGroup) {
-        val chipInnerView = view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner)
+        val chipInnerView = view.requireViewById<ViewGroup>(R.id.chipbar_inner)
         ViewHierarchyAnimator.animateAddition(
             chipInnerView,
             ViewHierarchyAnimator.Hotspot.TOP,
@@ -175,7 +179,7 @@
 
     override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
         ViewHierarchyAnimator.animateRemoval(
-            view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner),
+            view.requireViewById<ViewGroup>(R.id.chipbar_inner),
             ViewHierarchyAnimator.Hotspot.TOP,
             Interpolators.EMPHASIZED_ACCELERATE,
             ANIMATION_DURATION,
@@ -197,13 +201,5 @@
     }
 }
 
-data class ChipSenderInfo(
-    val state: ChipStateSender,
-    val routeInfo: MediaRoute2Info,
-    val undoCallback: IUndoMediaTransferCallback? = null
-) : TemporaryViewInfo {
-    override fun getTimeoutMs() = state.timeout
-}
-
 const val SENDER_TAG = "MediaTapToTransferSender"
 private const val ANIMATION_DURATION = 500L
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
new file mode 100644
index 0000000..57fde87
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import android.os.VibrationEffect
+import android.view.View
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.temporarydisplay.TemporaryViewInfo
+
+/**
+ * A container for all the state needed to display a chipbar via [ChipbarCoordinator].
+ *
+ * @property startIcon the icon to display at the start of the chipbar (on the left in LTR locales;
+ * on the right in RTL locales).
+ * @property text the text to display.
+ * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR
+ * locales; on the left in RTL locales).
+ * @property vibrationEffect an optional vibration effect when the chipbar is displayed
+ */
+data class ChipbarInfo(
+    val startIcon: Icon,
+    val text: Text,
+    val endItem: ChipbarEndItem?,
+    val vibrationEffect: VibrationEffect? = null,
+) : TemporaryViewInfo
+
+/** The possible items to display at the end of the chipbar. */
+sealed class ChipbarEndItem {
+    /** A loading icon should be displayed. */
+    object Loading : ChipbarEndItem()
+
+    /** An error icon should be displayed. */
+    object Error : ChipbarEndItem()
+
+    /**
+     * A button with the provided [text] and [onClickListener] functionality should be displayed.
+     */
+    data class Button(val text: Text, val onClickListener: View.OnClickListener) : ChipbarEndItem()
+
+    // TODO(b/245610654): Add support for a generic icon.
+}
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 3d56f23..3ecb15b 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -79,6 +79,7 @@
 import org.json.JSONObject;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
@@ -114,6 +115,7 @@
     private final SecureSettings mSecureSettings;
     private final Executor mMainExecutor;
     private final Handler mBgHandler;
+    private final boolean mIsMonochromaticEnabled;
     private final Context mContext;
     private final boolean mIsMonetEnabled;
     private final UserTracker mUserTracker;
@@ -363,6 +365,7 @@
             UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
             @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
         mContext = context;
+        mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES);
         mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
         mDeviceProvisionedController = deviceProvisionedController;
         mBroadcastDispatcher = broadcastDispatcher;
@@ -665,8 +668,13 @@
         // Allow-list of Style objects that can be created from a setting string, i.e. can be
         // used as a system-wide theme.
         // - Content intentionally excluded, intended for media player, not system-wide
-        List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
-                Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT);
+        List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ,
+                Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT));
+
+        if (mIsMonochromaticEnabled) {
+            validStyles.add(Style.MONOCHROMATIC);
+        }
+
         Style style = mThemeStyle;
         final String overlayPackageJson = mSecureSettings.getStringForUser(
                 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
index 51541bd..fda5114 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.toast
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.ToastLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogMessage
 import javax.inject.Inject
 
 private const val TAG = "ToastLog"
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
index ee785b6..088cd93 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
@@ -36,9 +36,7 @@
     private var adapter: ListAdapter? = null
 
     init {
-        setBackgroundDrawable(
-            res.getDrawable(R.drawable.bouncer_user_switcher_popup_bg, context.getTheme())
-        )
+        setBackgroundDrawable(null)
         setModal(false)
         setOverlapAnchor(true)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 919e699..b16dc54 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -220,7 +220,12 @@
             val result = withContext(backgroundDispatcher) { manager.aliveUsers }
 
             if (result != null) {
-                _userInfos.value = result.sortedBy { it.creationTime }
+                _userInfos.value =
+                    result
+                        // Users should be sorted by ascending creation time.
+                        .sortedBy { it.creationTime }
+                        // The guest user is always last, regardless of creation time.
+                        .sortedBy { it.isGuest }
             }
         }
     }
@@ -321,6 +326,7 @@
         return when {
             isAddUser -> false
             isAddSupervisedUser -> false
+            isManageUsers -> false
             isGuest -> info != null
             else -> true
         }
@@ -346,6 +352,7 @@
             isAddUser -> UserActionModel.ADD_USER
             isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
             isGuest -> UserActionModel.ENTER_GUEST_MODE
+            isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
             else -> error("Don't know how to convert to UserActionModel: $this")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index ba5a82a..dda78aa 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -236,18 +236,7 @@
                     }
                     .flatMapLatest { isActionable ->
                         if (isActionable) {
-                            repository.actions.map { actions ->
-                                actions +
-                                    if (actions.isNotEmpty()) {
-                                        // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
-                                        // because that's a user switcher specific action that is
-                                        // not known to the our data source or other features.
-                                        listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                                    } else {
-                                        // If no actions, don't add the navigate action.
-                                        emptyList()
-                                    }
-                            }
+                            repository.actions
                         } else {
                             // If not actionable it means that we're not allowed to show actions
                             // when
@@ -440,6 +429,7 @@
                         isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
                         isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
                         onExitGuestUser = this::exitGuestUser,
+                        dialogShower = dialogShower,
                     )
                 )
                 return
@@ -454,6 +444,7 @@
                         isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
                         isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
                         onExitGuestUser = this::exitGuestUser,
+                        dialogShower = dialogShower,
                     )
                 )
                 return
@@ -488,6 +479,7 @@
                             userHandle = currentUser.userHandle,
                             isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
                             showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
+                            dialogShower = dialogShower,
                         )
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
index 08d7c5a..177356e 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -18,14 +18,18 @@
 package com.android.systemui.user.domain.model
 
 import android.os.UserHandle
+import com.android.systemui.qs.user.UserSwitchDialogController
 
 /** Encapsulates a request to show a dialog. */
-sealed class ShowDialogRequestModel {
+sealed class ShowDialogRequestModel(
+    open val dialogShower: UserSwitchDialogController.DialogShower? = null,
+) {
     data class ShowAddUserDialog(
         val userHandle: UserHandle,
         val isKeyguardShowing: Boolean,
         val showEphemeralMessage: Boolean,
-    ) : ShowDialogRequestModel()
+        override val dialogShower: UserSwitchDialogController.DialogShower?,
+    ) : ShowDialogRequestModel(dialogShower)
 
     data class ShowUserCreationDialog(
         val isGuest: Boolean,
@@ -37,5 +41,6 @@
         val isGuestEphemeral: Boolean,
         val isKeyguardShowing: Boolean,
         val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
-    ) : ShowDialogRequestModel()
+        override val dialogShower: UserSwitchDialogController.DialogShower?,
+    ) : ShowDialogRequestModel(dialogShower)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
index 938417f..968af59 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
@@ -18,12 +18,15 @@
 package com.android.systemui.user.ui.binder
 
 import android.content.Context
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
 import android.widget.BaseAdapter
 import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.LinearLayout.SHOW_DIVIDER_MIDDLE
 import android.widget.TextView
 import androidx.constraintlayout.helper.widget.Flow as FlowWidget
 import androidx.core.view.isVisible
@@ -36,6 +39,7 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.user.UserSwitcherPopupMenu
 import com.android.systemui.user.UserSwitcherRootView
+import com.android.systemui.user.shared.model.UserActionModel
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.util.children
@@ -168,15 +172,10 @@
         onDismissed: () -> Unit,
     ): UserSwitcherPopupMenu {
         return UserSwitcherPopupMenu(context).apply {
+            this.setDropDownGravity(Gravity.END)
             this.anchorView = anchorView
             setAdapter(adapter)
             setOnDismissListener { onDismissed() }
-            setOnItemClickListener { _, _, position, _ ->
-                val itemPositionExcludingHeader = position - 1
-                adapter.getItem(itemPositionExcludingHeader).onClicked()
-                dismiss()
-            }
-
             show()
         }
     }
@@ -186,38 +185,67 @@
         private val layoutInflater: LayoutInflater,
     ) : BaseAdapter() {
 
-        private val items = mutableListOf<UserActionViewModel>()
+        private var sections = listOf<List<UserActionViewModel>>()
 
         override fun getCount(): Int {
-            return items.size
+            return sections.size
         }
 
-        override fun getItem(position: Int): UserActionViewModel {
-            return items[position]
+        override fun getItem(position: Int): List<UserActionViewModel> {
+            return sections[position]
         }
 
         override fun getItemId(position: Int): Long {
-            return getItem(position).viewKey
+            return position.toLong()
         }
 
         override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
-            val view =
-                convertView
-                    ?: layoutInflater.inflate(
+            val section = getItem(position)
+            val context = parent.context
+            val sectionView =
+                convertView as? LinearLayout
+                    ?: LinearLayout(context, null).apply {
+                        this.orientation = LinearLayout.VERTICAL
+                        this.background =
+                            parent.resources.getDrawable(
+                                R.drawable.bouncer_user_switcher_popup_bg,
+                                context.theme
+                            )
+                        this.showDividers = SHOW_DIVIDER_MIDDLE
+                        this.dividerDrawable =
+                            context.getDrawable(
+                                R.drawable.fullscreen_userswitcher_menu_item_divider
+                            )
+                    }
+            sectionView.removeAllViewsInLayout()
+
+            for (viewModel in section) {
+                val view =
+                    layoutInflater.inflate(
                         R.layout.user_switcher_fullscreen_popup_item,
-                        parent,
-                        false
+                        /* parent= */ null
                     )
-            val viewModel = getItem(position)
-            view.requireViewById<ImageView>(R.id.icon).setImageResource(viewModel.iconResourceId)
-            view.requireViewById<TextView>(R.id.text).text =
-                view.resources.getString(viewModel.textResourceId)
-            return view
+                view
+                    .requireViewById<ImageView>(R.id.icon)
+                    .setImageResource(viewModel.iconResourceId)
+                view.requireViewById<TextView>(R.id.text).text =
+                    view.resources.getString(viewModel.textResourceId)
+                view.setOnClickListener { viewModel.onClicked() }
+                sectionView.addView(view)
+            }
+            return sectionView
         }
 
         fun setItems(items: List<UserActionViewModel>) {
-            this.items.clear()
-            this.items.addAll(items)
+            val primarySection =
+                items.filter {
+                    it.viewKey != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong()
+                }
+            val secondarySection =
+                items.filter {
+                    it.viewKey == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong()
+                }
+            this.sections = listOf(primarySection, secondarySection)
             notifyDataSetChanged()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
index 91c5921..e921720 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -19,8 +19,10 @@
 
 import android.app.Dialog
 import android.content.Context
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.settingslib.users.UserCreatingDialog
 import com.android.systemui.CoreStartable
+import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dagger.SysUISingleton
@@ -30,6 +32,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.collect
@@ -41,19 +44,19 @@
 class UserSwitcherDialogCoordinator
 @Inject
 constructor(
-    @Application private val context: Context,
-    @Application private val applicationScope: CoroutineScope,
-    private val falsingManager: FalsingManager,
-    private val broadcastSender: BroadcastSender,
-    private val dialogLaunchAnimator: DialogLaunchAnimator,
-    private val interactor: UserInteractor,
-    private val featureFlags: FeatureFlags,
+    @Application private val context: Lazy<Context>,
+    @Application private val applicationScope: Lazy<CoroutineScope>,
+    private val falsingManager: Lazy<FalsingManager>,
+    private val broadcastSender: Lazy<BroadcastSender>,
+    private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>,
+    private val interactor: Lazy<UserInteractor>,
+    private val featureFlags: Lazy<FeatureFlags>,
 ) : CoreStartable {
 
     private var currentDialog: Dialog? = null
 
     override fun start() {
-        if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
+        if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
             return
         }
 
@@ -62,61 +65,87 @@
     }
 
     private fun startHandlingDialogShowRequests() {
-        applicationScope.launch {
-            interactor.dialogShowRequests.filterNotNull().collect { request ->
+        applicationScope.get().launch {
+            interactor.get().dialogShowRequests.filterNotNull().collect { request ->
                 currentDialog?.let {
                     if (it.isShowing) {
                         it.cancel()
                     }
                 }
 
-                currentDialog =
+                val (dialog, dialogCuj) =
                     when (request) {
                         is ShowDialogRequestModel.ShowAddUserDialog ->
-                            AddUserDialog(
-                                context = context,
-                                userHandle = request.userHandle,
-                                isKeyguardShowing = request.isKeyguardShowing,
-                                showEphemeralMessage = request.showEphemeralMessage,
-                                falsingManager = falsingManager,
-                                broadcastSender = broadcastSender,
-                                dialogLaunchAnimator = dialogLaunchAnimator,
+                            Pair(
+                                AddUserDialog(
+                                    context = context.get(),
+                                    userHandle = request.userHandle,
+                                    isKeyguardShowing = request.isKeyguardShowing,
+                                    showEphemeralMessage = request.showEphemeralMessage,
+                                    falsingManager = falsingManager.get(),
+                                    broadcastSender = broadcastSender.get(),
+                                    dialogLaunchAnimator = dialogLaunchAnimator.get(),
+                                ),
+                                DialogCuj(
+                                    InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+                                    INTERACTION_JANK_ADD_NEW_USER_TAG,
+                                ),
                             )
                         is ShowDialogRequestModel.ShowUserCreationDialog ->
-                            UserCreatingDialog(
-                                context,
-                                request.isGuest,
+                            Pair(
+                                UserCreatingDialog(
+                                    context.get(),
+                                    request.isGuest,
+                                ),
+                                null,
                             )
                         is ShowDialogRequestModel.ShowExitGuestDialog ->
-                            ExitGuestDialog(
-                                context = context,
-                                guestUserId = request.guestUserId,
-                                isGuestEphemeral = request.isGuestEphemeral,
-                                targetUserId = request.targetUserId,
-                                isKeyguardShowing = request.isKeyguardShowing,
-                                falsingManager = falsingManager,
-                                dialogLaunchAnimator = dialogLaunchAnimator,
-                                onExitGuestUserListener = request.onExitGuestUser,
+                            Pair(
+                                ExitGuestDialog(
+                                    context = context.get(),
+                                    guestUserId = request.guestUserId,
+                                    isGuestEphemeral = request.isGuestEphemeral,
+                                    targetUserId = request.targetUserId,
+                                    isKeyguardShowing = request.isKeyguardShowing,
+                                    falsingManager = falsingManager.get(),
+                                    dialogLaunchAnimator = dialogLaunchAnimator.get(),
+                                    onExitGuestUserListener = request.onExitGuestUser,
+                                ),
+                                DialogCuj(
+                                    InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+                                    INTERACTION_JANK_EXIT_GUEST_MODE_TAG,
+                                ),
                             )
                     }
+                currentDialog = dialog
 
-                currentDialog?.show()
-                interactor.onDialogShown()
+                if (request.dialogShower != null && dialogCuj != null) {
+                    request.dialogShower?.showDialog(dialog, dialogCuj)
+                } else {
+                    dialog.show()
+                }
+
+                interactor.get().onDialogShown()
             }
         }
     }
 
     private fun startHandlingDialogDismissRequests() {
-        applicationScope.launch {
-            interactor.dialogDismissRequests.filterNotNull().collect {
+        applicationScope.get().launch {
+            interactor.get().dialogDismissRequests.filterNotNull().collect {
                 currentDialog?.let {
                     if (it.isShowing) {
                         it.cancel()
                     }
                 }
 
-                interactor.onDialogDismissed()
+                interactor.get().onDialogDismissed()
             }
         }
     }
+
+    companion object {
+        private const val INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user"
+        private const val INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 219dae2..d857e85 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -62,17 +62,7 @@
     val isMenuVisible: Flow<Boolean> = _isMenuVisible
     /** The user action menu. */
     val menu: Flow<List<UserActionViewModel>> =
-        userInteractor.actions.map { actions ->
-            if (isNewImpl && actions.isNotEmpty()) {
-                    // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
-                    // switcher specific action that is not known to the our data source or other
-                    // features.
-                    actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                } else {
-                    actions
-                }
-                .map { action -> toViewModel(action) }
-        }
+        userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
 
     /** Whether the button to open the user action menu is visible. */
     val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
index ecb365f..2c317dd 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
@@ -172,10 +172,14 @@
         return Boolean.TRUE.equals(mIsConditionMet);
     }
 
-    private boolean shouldLog() {
+    protected final boolean shouldLog() {
         return Log.isLoggable(mTag, Log.DEBUG);
     }
 
+    protected final String getTag() {
+        return mTag;
+    }
+
     /**
      * Callback that receives updates about whether the condition has been fulfilled.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
index 4824f67..cb430ba 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
@@ -117,6 +117,7 @@
         final SubscriptionState state = new SubscriptionState(subscription);
 
         mExecutor.execute(() -> {
+            if (shouldLog()) Log.d(mTag, "adding subscription");
             mSubscriptions.put(token, state);
 
             // Add and associate conditions.
@@ -143,7 +144,7 @@
      */
     public void removeSubscription(@NotNull Subscription.Token token) {
         mExecutor.execute(() -> {
-            if (shouldLog()) Log.d(mTag, "removing callback");
+            if (shouldLog()) Log.d(mTag, "removing subscription");
             if (!mSubscriptions.containsKey(token)) {
                 Log.e(mTag, "subscription not present:" + token);
                 return;
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
similarity index 77%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
rename to packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
index 23072a2..b7166d9 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
@@ -14,8 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.gallery
+syntax = "proto3";
 
-import com.android.settingslib.spa.framework.DebugActivity
+package com.android.systemui.util;
 
-class GalleryDebugActivity : DebugActivity()
+option java_multiple_files = true;
+
+message ComponentNameProto {
+  string package_name = 1;
+  string class_name = 2;
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
new file mode 100644
index 0000000..7b9b39f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.keyguard
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.AttributeSet
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class BouncerKeyguardMessageAreaTest : SysuiTestCase() {
+    class FakeBouncerKeyguardMessageArea(context: Context, attrs: AttributeSet?) :
+        BouncerKeyguardMessageArea(context, attrs) {
+        override val SHOW_DURATION_MILLIS = 0L
+        override val HIDE_DURATION_MILLIS = 0L
+    }
+    lateinit var underTest: BouncerKeyguardMessageArea
+
+    @Before
+    fun setup() {
+        underTest = FakeBouncerKeyguardMessageArea(context, null)
+    }
+
+    @Test
+    fun testSetSameMessage() {
+        val underTestSpy = spy(underTest)
+        underTestSpy.setMessage("abc")
+        underTestSpy.setMessage("abc")
+        verify(underTestSpy, times(1)).text = "abc"
+    }
+
+    @Test
+    fun testSetDifferentMessage() {
+        underTest.setMessage("abc")
+        underTest.setMessage("def")
+        assertThat(underTest.text).isEqualTo("def")
+    }
+
+    @Test
+    fun testSetNullMessage() {
+        underTest.setMessage(null)
+        assertThat(underTest.text).isEqualTo("")
+    }
+
+    @Test
+    fun testSetNullClearsPreviousMessage() {
+        underTest.setMessage("something not null")
+        assertThat(underTest.text).isEqualTo("something not null")
+
+        underTest.setMessage(null)
+        assertThat(underTest.text).isEqualTo("")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 8a2c354..03efd06 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -17,17 +17,21 @@
 
 import android.content.BroadcastReceiver
 import android.testing.AndroidTestingRunner
+import android.view.View
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.plugins.ClockAnimations
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
-import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.mockito.any
@@ -37,6 +41,9 @@
 import com.android.systemui.util.mockito.mock
 import java.util.TimeZone
 import java.util.concurrent.Executor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
@@ -57,7 +64,7 @@
 class ClockEventControllerTest : SysuiTestCase() {
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
-    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var keyguardInteractor: KeyguardInteractor
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock private lateinit var batteryController: BatteryController
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@@ -72,8 +79,11 @@
     @Mock private lateinit var largeClockController: ClockFaceController
     @Mock private lateinit var smallClockEvents: ClockFaceEvents
     @Mock private lateinit var largeClockEvents: ClockFaceEvents
+    @Mock private lateinit var parentView: View
+    @Mock private lateinit var transitionRepository: KeyguardTransitionRepository
+    private lateinit var repository: FakeKeyguardRepository
 
-    private lateinit var clockEventController: ClockEventController
+    private lateinit var underTest: ClockEventController
 
     @Before
     fun setUp() {
@@ -86,8 +96,11 @@
         whenever(clock.events).thenReturn(events)
         whenever(clock.animations).thenReturn(animations)
 
-        clockEventController = ClockEventController(
-            statusBarStateController,
+        repository = FakeKeyguardRepository()
+
+        underTest = ClockEventController(
+            KeyguardInteractor(repository = repository),
+            KeyguardTransitionInteractor(repository = transitionRepository),
             broadcastDispatcher,
             batteryController,
             keyguardUpdateMonitor,
@@ -98,31 +111,33 @@
             bgExecutor,
             featureFlags
         )
+        underTest.clock = clock
+
+        runBlocking(IMMEDIATE) {
+            underTest.registerListeners(parentView)
+
+            repository.setDozing(true)
+            repository.setDozeAmount(1f)
+        }
     }
 
     @Test
     fun clockSet_validateInitialization() {
-        clockEventController.clock = clock
-
         verify(clock).initialize(any(), anyFloat(), anyFloat())
     }
 
     @Test
     fun clockUnset_validateState() {
-        clockEventController.clock = clock
-        clockEventController.clock = null
+        underTest.clock = null
 
-        assertEquals(clockEventController.clock, null)
+        assertEquals(underTest.clock, null)
     }
 
     @Test
-    fun themeChanged_verifyClockPaletteUpdated() {
-        clockEventController.clock = clock
+    fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
         verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
         verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
 
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
         captor.value.onThemeChanged()
@@ -131,13 +146,10 @@
     }
 
     @Test
-    fun fontChanged_verifyFontSizeUpdated() {
-        clockEventController.clock = clock
+    fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) {
         verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
         verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
 
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
         captor.value.onDensityOrFontScaleChanged()
@@ -146,10 +158,7 @@
     }
 
     @Test
-    fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
         val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
         verify(batteryController).addCallback(capture(batteryCaptor))
         val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -161,26 +170,21 @@
     }
 
     @Test
-    fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() =
+        runBlocking(IMMEDIATE) {
+            val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+            verify(batteryController).addCallback(capture(batteryCaptor))
+            val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+            verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+            keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, true)
 
-        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
-        verify(batteryController).addCallback(capture(batteryCaptor))
-        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
-        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
-        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
-
-        verify(animations, times(1)).charge()
-    }
+            verify(animations, times(1)).charge()
+        }
 
     @Test
-    fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
         val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
         verify(batteryController).addCallback(capture(batteryCaptor))
         val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -192,25 +196,20 @@
     }
 
     @Test
-    fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() =
+        runBlocking(IMMEDIATE) {
+            val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+            verify(batteryController).addCallback(capture(batteryCaptor))
+            val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+            verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+            keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, false)
 
-        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
-        verify(batteryController).addCallback(capture(batteryCaptor))
-        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
-        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
-        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, false)
-
-        verify(animations, never()).charge()
-    }
+            verify(animations, never()).charge()
+        }
 
     @Test
-    fun localeCallback_verifyClockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<BroadcastReceiver>()
         verify(broadcastDispatcher).registerReceiver(
             capture(captor), any(), eq(null), eq(null), anyInt(), eq(null)
@@ -221,10 +220,7 @@
     }
 
     @Test
-    fun keyguardCallback_visibilityChanged_clockDozeCalled() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
 
@@ -236,10 +232,7 @@
     }
 
     @Test
-    fun keyguardCallback_timeFormat_clockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onTimeFormatChanged("12h")
@@ -248,11 +241,8 @@
     }
 
     @Test
-    fun keyguardCallback_timezoneChanged_clockNotified() {
+    fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) {
         val mockTimeZone = mock<TimeZone>()
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onTimeZoneChanged(mockTimeZone)
@@ -261,10 +251,7 @@
     }
 
     @Test
-    fun keyguardCallback_userSwitched_clockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onUserSwitchComplete(10)
@@ -273,25 +260,27 @@
     }
 
     @Test
-    fun keyguardCallback_verifyKeyguardChanged() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) {
+        val job = underTest.listenForDozeAmount(this)
+        repository.setDozeAmount(0.4f)
 
-        val captor = argumentCaptor<StatusBarStateController.StateListener>()
-        verify(statusBarStateController).addCallback(capture(captor))
-        captor.value.onDozeAmountChanged(0.4f, 0.6f)
+        yield()
 
         verify(animations).doze(0.4f)
+
+        job.cancel()
     }
 
     @Test
-    fun unregisterListeners_validate() {
-        clockEventController.clock = clock
-        clockEventController.unregisterListeners()
+    fun unregisterListeners_validate() = runBlocking(IMMEDIATE) {
+        underTest.unregisterListeners()
         verify(broadcastDispatcher).unregisterReceiver(any())
         verify(configurationController).removeCallback(any())
         verify(batteryController).removeCallback(any())
         verify(keyguardUpdateMonitor).removeCallback(any())
-        verify(statusBarStateController).removeCallback(any())
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 9b2bba6..627d738 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -280,6 +280,6 @@
     private void verifyAttachment(VerificationMode times) {
         verify(mClockRegistry, times).registerClockChangeListener(
                 any(ClockRegistry.ClockChangeListener.class));
-        verify(mClockEventController, times).registerListeners();
+        verify(mClockEventController, times).registerListeners(mView);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
index 69524e5..5d2b0ca 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
@@ -17,13 +17,11 @@
 package com.android.keyguard;
 
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 
-import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -92,19 +90,4 @@
         mMessageAreaController.setIsVisible(true);
         verify(mKeyguardMessageArea).setIsVisible(true);
     }
-
-    @Test
-    public void testSetMessageIfEmpty_empty() {
-        mMessageAreaController.setMessage("");
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        verify(mKeyguardMessageArea).setMessage(R.string.keyguard_enter_your_pin);
-    }
-
-    @Test
-    public void testSetMessageIfEmpty_notEmpty() {
-        mMessageAreaController.setMessage("abc");
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        verify(mKeyguardMessageArea, never()).setMessage(getContext()
-                .getResources().getText(R.string.keyguard_enter_your_pin));
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index b89dbd9..b369098 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -114,9 +114,8 @@
     }
 
     @Test
-    fun onResume_testSetInitialText() {
-        keyguardPasswordViewController.onResume(KeyguardSecurityView.SCREEN_ON)
-        verify(mKeyguardMessageAreaController)
-            .setMessageIfEmpty(R.string.keyguard_enter_your_password)
+    fun startAppearAnimation() {
+        keyguardPasswordViewController.startAppearAnimation()
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 3262a77..9eff704 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -100,16 +100,16 @@
     }
 
     @Test
-    fun onPause_clearsTextField() {
+    fun onPause_resetsText() {
         mKeyguardPatternViewController.init()
         mKeyguardPatternViewController.onPause()
-        verify(mKeyguardMessageAreaController).setMessage("")
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
     }
 
+
     @Test
-    fun onResume_setInitialText() {
-        mKeyguardPatternViewController.onResume(KeyguardSecurityView.SCREEN_ON)
-        verify(mKeyguardMessageAreaController)
-            .setMessageIfEmpty(R.string.keyguard_enter_your_pattern)
+    fun startAppearAnimation() {
+        mKeyguardPatternViewController.startAppearAnimation()
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index 97d556b..ce1101f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -113,11 +113,4 @@
         mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
         verify(mPasswordEntry).requestFocus();
     }
-
-    @Test
-    public void onResume_setInitialText() {
-        mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
-        verify(mKeyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-    }
 }
-
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 9e5bfe5..d9efdea 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -98,6 +98,6 @@
     @Test
     fun startAppearAnimation() {
         pinViewController.startAppearAnimation()
-        verify(keyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin)
+        verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 48e8239..b885d54 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -146,6 +146,8 @@
 
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback;
+    @Captor
+    private ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> mSwipeListenerArgumentCaptor;
 
     private Configuration mConfiguration;
 
@@ -475,6 +477,64 @@
         verify(mKeyguardUpdateMonitor, never()).getUserHasTrust(anyInt());
     }
 
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(false);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardUpdateMonitor).requestFaceAuth(true,
+                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsRunning_doesNotInitiateFaceAuth() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(true);
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardUpdateMonitor, never())
+                .requestFaceAuth(true,
+                        FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.requestFaceAuth(true,
+                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(true);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardPasswordViewControllerMock).showMessage(null, null);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.requestFaceAuth(true,
+                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(false);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardPasswordViewControllerMock, never()).showMessage(null, null);
+    }
+
+    private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() {
+        mKeyguardSecurityContainerController.onViewAttached();
+        verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture());
+        return mSwipeListenerArgumentCaptor.getValue();
+    }
+
     private void setupConditionsToEnableSideFpsHint() {
         attachView();
         setSideFpsHintEnabledFromResources(true);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 7281bc8..c6233b5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -26,6 +26,7 @@
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT;
+import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
 import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -648,6 +649,36 @@
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT);
     }
 
+    @Test
+    public void requestFaceAuth_whenFaceAuthWasStarted_returnsTrue() throws RemoteException {
+        // This satisfies all the preconditions to run face auth.
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+        bouncerFullyVisibleAndNotGoingToSleep();
+        mTestableLooper.processAllMessages();
+
+        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true,
+                NOTIFICATION_PANEL_CLICKED);
+
+        assertThat(didFaceAuthRun).isTrue();
+    }
+
+    @Test
+    public void requestFaceAuth_whenFaceAuthWasNotStarted_returnsFalse() throws RemoteException {
+        // This ensures face auth won't run.
+        biometricsDisabledForCurrentUser();
+        mTestableLooper.processAllMessages();
+
+        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true,
+                NOTIFICATION_PANEL_CLICKED);
+
+        assertThat(didFaceAuthRun).isFalse();
+    }
+
     private void testStrongAuthExceptOnBouncer(int strongAuth) {
         when(mKeyguardBypassController.canBypass()).thenReturn(true);
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
new file mode 100644
index 0000000..6391a2c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.PointF;
+import android.testing.AndroidTestingRunner;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link MenuAnimationController}. */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class MenuAnimationControllerTest extends SysuiTestCase {
+    private MenuView mMenuView;
+    private MenuAnimationController mMenuAnimationController;
+
+    @Before
+    public void setUp() throws Exception {
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                stubWindowManager);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        mMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance);
+        mMenuAnimationController = new MenuAnimationController(mMenuView);
+    }
+
+    @Test
+    public void moveToPosition_matchPosition() {
+        final PointF destination = new PointF(50, 60);
+
+        mMenuAnimationController.moveToPosition(destination);
+
+        assertThat(mMenuView.getTranslationX()).isEqualTo(50);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(60);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
index d8b10e0..e62a329 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility.floatingmenu;
 
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.verify;
 
 import android.testing.AndroidTestingRunner;
@@ -25,6 +26,7 @@
 
 import com.android.systemui.SysuiTestCase;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,13 +44,24 @@
     @Mock
     private MenuInfoRepository.OnSettingsContentsChanged mMockSettingsContentsChanged;
 
+    private MenuInfoRepository mMenuInfoRepository;
+
+    @Before
+    public void setUp() {
+        mMenuInfoRepository = new MenuInfoRepository(mContext, mMockSettingsContentsChanged);
+    }
+
     @Test
     public void menuSizeTypeChanged_verifyOnSizeTypeChanged() {
-        final MenuInfoRepository menuInfoRepository =
-                new MenuInfoRepository(mContext,  mMockSettingsContentsChanged);
-
-        menuInfoRepository.mMenuSizeContentObserver.onChange(true);
+        mMenuInfoRepository.mMenuSizeContentObserver.onChange(true);
 
         verify(mMockSettingsContentsChanged).onSizeTypeChanged(anyInt());
     }
+
+    @Test
+    public void menuOpacityChanged_verifyOnFadeEffectChanged() {
+        mMenuInfoRepository.mMenuFadeOutContentObserver.onChange(true);
+
+        verify(mMockSettingsContentsChanged).onFadeEffectInfoChanged(any(MenuFadeEffectInfo.class));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
new file mode 100644
index 0000000..bf6d574
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link MenuItemAccessibilityDelegate}. */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class MenuItemAccessibilityDelegateTest extends SysuiTestCase {
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    private RecyclerView mStubListView;
+    private MenuView mMenuView;
+    private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate;
+    private MenuAnimationController mMenuAnimationController;
+    private final Rect mDraggableBounds = new Rect(100, 200, 300, 400);
+
+    @Before
+    public void setUp() {
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                stubWindowManager);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+
+        final int halfScreenHeight =
+                stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2;
+        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
+        mMenuView.setTranslationY(halfScreenHeight);
+
+        doReturn(mDraggableBounds).when(mMenuView).getMenuDraggableBounds();
+        mStubListView = new RecyclerView(mContext);
+        mMenuAnimationController = spy(new MenuAnimationController(mMenuView));
+        mMenuItemAccessibilityDelegate =
+                new MenuItemAccessibilityDelegate(new RecyclerViewAccessibilityDelegate(
+                        mStubListView), mMenuAnimationController);
+    }
+
+    @Test
+    public void getAccessibilityActionList_matchSize() {
+        final AccessibilityNodeInfoCompat info =
+                new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
+
+        mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info);
+
+        assertThat(info.getActionList().size()).isEqualTo(5);
+    }
+
+    @Test
+    public void performMoveTopLeftAction_matchPosition() {
+        final boolean moveTopLeftAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_top_left,
+                        null);
+
+        assertThat(moveTopLeftAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top);
+    }
+
+    @Test
+    public void performMoveTopRightAction_matchPosition() {
+        final boolean moveTopRightAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_top_right, null);
+
+        assertThat(moveTopRightAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top);
+    }
+
+    @Test
+    public void performMoveBottomLeftAction_matchPosition() {
+        final boolean moveBottomLeftAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_bottom_left, null);
+
+        assertThat(moveBottomLeftAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom);
+    }
+
+    @Test
+    public void performMoveBottomRightAction_matchPosition() {
+        final boolean moveBottomRightAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_bottom_right, null);
+
+        assertThat(moveBottomRightAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom);
+    }
+
+    @Test
+    public void performMoveToEdgeAndHideAction_success() {
+        final boolean moveToEdgeAndHideAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_to_edge_and_hide, null);
+
+        assertThat(moveToEdgeAndHideAction).isTrue();
+        verify(mMenuAnimationController).moveToEdgeAndHide();
+    }
+
+    @Test
+    public void performMoveOutFromEdgeAction_success() {
+        final boolean moveOutEdgeAndShowAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_out_edge_and_show, null);
+
+        assertThat(moveOutEdgeAndShowAction).isTrue();
+        verify(mMenuAnimationController).moveOutEdgeAndShow();
+    }
+
+    @Test
+    public void performFocusAction_fadeIn() {
+        mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                ACTION_ACCESSIBILITY_FOCUS, null);
+
+        verify(mMenuAnimationController).fadeInNowIfEnabled();
+    }
+
+    @Test
+    public void performClearFocusAction_fadeOut() {
+        mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
+
+        verify(mMenuAnimationController).fadeOutIfEnabled();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
new file mode 100644
index 0000000..c5b9a29
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static android.view.View.OVER_SCROLL_NEVER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.accessibility.dialog.AccessibilityTarget;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.accessibility.MotionEventHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link MenuListViewTouchHandler}. */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class MenuListViewTouchHandlerTest extends SysuiTestCase {
+    private final List<AccessibilityTarget> mStubTargets = new ArrayList<>(
+            Collections.singletonList(mock(AccessibilityTarget.class)));
+    private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+    private MenuView mStubMenuView;
+    private MenuListViewTouchHandler mTouchHandler;
+    private MenuAnimationController mMenuAnimationController;
+    private RecyclerView mStubListView;
+
+    @Before
+    public void setUp() throws Exception {
+        final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                windowManager);
+        mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance);
+        mStubMenuView.setTranslationX(0);
+        mStubMenuView.setTranslationY(0);
+        mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView));
+        mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController);
+        final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets);
+        mStubListView = (RecyclerView) mStubMenuView.getChildAt(0);
+        mStubListView.setAdapter(stubAdapter);
+    }
+
+    @Test
+    public void onActionDownEvent_shouldCancelAnimations() {
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+
+        verify(mMenuAnimationController).cancelAnimations();
+    }
+
+    @Test
+    public void onActionMoveEvent_shouldMoveToPosition() {
+        final int offset = 100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mStubListView.setOverScrollMode(OVER_SCROLL_NEVER);
+
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+
+        assertThat(mStubMenuView.getTranslationX()).isEqualTo(offset);
+        assertThat(mStubMenuView.getTranslationY()).isEqualTo(offset);
+    }
+
+    @Test
+    public void dragAndDrop_shouldFlingMenuThenSpringToEdge() {
+        final int offset = 100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        final MotionEvent stubUpEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 5,
+                        MotionEvent.ACTION_UP, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubUpEvent);
+
+        verify(mMenuAnimationController).flingMenuThenSpringToEdge(anyFloat(), anyFloat(),
+                anyFloat());
+    }
+
+    @Test
+    public void dragMenuOutOfBoundsAndDrop_moveToLeftEdge_shouldMoveToEdgeAndHide() {
+        final int offset = -100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        final MotionEvent stubUpEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 5,
+                        MotionEvent.ACTION_UP, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubUpEvent);
+
+        verify(mMenuAnimationController).moveToEdgeAndHide();
+    }
+
+    @After
+    public void tearDown() {
+        mMotionEventHelper.recycleEvents();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
index f782a44..8c8d6ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
@@ -16,13 +16,24 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
+import static android.view.WindowInsets.Type.displayCutout;
+import static android.view.WindowInsets.Type.systemBars;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.test.filters.SmallTest;
 
@@ -38,6 +49,7 @@
 
 /** Tests for {@link MenuViewLayerController}. */
 @RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 public class MenuViewLayerControllerTest extends SysuiTestCase {
     @Rule
@@ -46,10 +58,20 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private WindowMetrics mWindowMetrics;
+
     private MenuViewLayerController mMenuViewLayerController;
 
     @Before
     public void setUp() throws Exception {
+        final WindowManager wm = mContext.getSystemService(WindowManager.class);
+        doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
+                mWindowManager).getMaximumWindowMetrics();
+        mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340));
+        when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets());
         mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager);
     }
 
@@ -68,4 +90,14 @@
 
         verify(mWindowManager).removeView(any(View.class));
     }
+
+    private WindowInsets stubDisplayInsets() {
+        final int stubStatusBarHeight = 118;
+        final int stubNavigationBarHeight = 125;
+        return new WindowInsets.Builder()
+                .setVisible(systemBars() | displayCutout(), true)
+                .setInsets(systemBars() | displayCutout(),
+                        Insets.of(0, stubStatusBarHeight, 0, stubNavigationBarHeight))
+                .build();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index 8883cb7..23c6ef1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -26,6 +26,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
+import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -44,7 +45,8 @@
 
     @Before
     public void setUp() throws Exception {
-        mMenuViewLayer = new MenuViewLayer(mContext);
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
index 513044d..742ee53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
@@ -24,11 +24,15 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.UiModeManager;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.Prefs;
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.After;
@@ -45,6 +49,8 @@
     private int mNightMode;
     private UiModeManager mUiModeManager;
     private MenuView mMenuView;
+    private String mLastPosition;
+    private MenuViewAppearance mStubMenuViewAppearance;
 
     @Before
     public void setUp() throws Exception {
@@ -52,8 +58,11 @@
         mNightMode = mUiModeManager.getNightMode();
         mUiModeManager.setNightMode(MODE_NIGHT_YES);
         final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
-        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext);
-        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        mStubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager);
+        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, mStubMenuViewAppearance));
+        mLastPosition = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
     }
 
     @Test
@@ -74,8 +83,58 @@
         assertThat(areInsetsMatched).isTrue();
     }
 
+    @Test
+    public void onDraggingStart_matchInsets() {
+        mMenuView.onDraggingStart();
+        final InstantInsetLayerDrawable insetLayerDrawable =
+                (InstantInsetLayerDrawable) mMenuView.getBackground();
+
+        assertThat(insetLayerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetTop(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetRight(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetBottom(INDEX_MENU_ITEM)).isEqualTo(0);
+    }
+
+    @Test
+    public void onAnimationend_updatePositionForSharedPreference() {
+        final float percentageX = 0.0f;
+        final float percentageY = 0.5f;
+
+        mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+        final String positionString = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
+        final Position position = Position.fromString(positionString);
+
+        assertThat(position.getPercentageX()).isEqualTo(percentageX);
+        assertThat(position.getPercentageY()).isEqualTo(percentageY);
+    }
+
+    @Test
+    public void onEdgeChangedIfNeeded_moveToLeftEdge_matchRadii() {
+        final Rect draggableBounds = mStubMenuViewAppearance.getMenuDraggableBounds();
+        mMenuView.setTranslationX(draggableBounds.right);
+
+        mMenuView.setTranslationX(draggableBounds.left);
+        mMenuView.onEdgeChangedIfNeeded();
+        final float[] radii = getMenuViewGradient().getCornerRadii();
+
+        assertThat(radii[0]).isEqualTo(0.0f);
+        assertThat(radii[1]).isEqualTo(0.0f);
+        assertThat(radii[6]).isEqualTo(0.0f);
+        assertThat(radii[7]).isEqualTo(0.0f);
+    }
+
+    private InstantInsetLayerDrawable getMenuViewInsetLayer() {
+        return (InstantInsetLayerDrawable) mMenuView.getBackground();
+    }
+
+    private GradientDrawable getMenuViewGradient() {
+        return (GradientDrawable) getMenuViewInsetLayer().getDrawable(INDEX_MENU_ITEM);
+    }
+
     @After
     public void tearDown() throws Exception {
         mUiModeManager.setNightMode(mNightMode);
+        Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, mLastPosition);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index baeabc5..d489656 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -26,6 +26,7 @@
 import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
+import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.LayoutInflater
@@ -124,14 +125,18 @@
         whenever(udfpsEnrollView.context).thenReturn(context)
     }
 
-    private fun withReason(@ShowReason reason: Int, block: () -> Unit) {
+    private fun withReason(
+        @ShowReason reason: Int,
+        isDebuggable: Boolean = false,
+        block: () -> Unit
+    ) {
         controllerOverlay = UdfpsControllerOverlay(
             context, fingerprintManager, inflater, windowManager, accessibilityManager,
             statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager,
             keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
             configurationController, systemClock, keyguardStateController,
             unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason,
-            controllerCallback, onTouch, activityLaunchAnimator
+            controllerCallback, onTouch, activityLaunchAnimator, isDebuggable
         )
         block()
     }
@@ -151,11 +156,29 @@
     }
 
     @Test
+    fun showUdfpsOverlay_locate_withEnrollmentUiRemoved() {
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1)
+        withReason(REASON_ENROLL_FIND_SENSOR, isDebuggable = true) {
+            showUdfpsOverlay(isEnrollUseCase = false)
+        }
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0)
+    }
+
+    @Test
     fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) {
         showUdfpsOverlay(isEnrollUseCase = true)
     }
 
     @Test
+    fun showUdfpsOverlay_enroll_withEnrollmentUiRemoved() {
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1)
+        withReason(REASON_ENROLL_ENROLLING, isDebuggable = true) {
+            showUdfpsOverlay(isEnrollUseCase = false)
+        }
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0)
+    }
+
+    @Test
     fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() }
 
     private fun withRotation(@Rotation rotation: Int, block: () -> Unit) {
@@ -163,6 +186,7 @@
         val sensorBounds = Rect(0, 0, SENSOR_WIDTH, SENSOR_HEIGHT)
         overlayParams = UdfpsOverlayParams(
             sensorBounds,
+            sensorBounds,
             DISPLAY_WIDTH,
             DISPLAY_HEIGHT,
             scaleFactor = 1f,
@@ -372,21 +396,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_0
         // touch at 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
         // touch at 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
         // touch at 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
     }
 
     fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) {
@@ -394,21 +430,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_90
         // touch at 0 degrees -> 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 90 degrees -> 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
         // touch at 180 degrees -> 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
         // touch at 270 degrees -> 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
     }
 
     fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) {
@@ -416,21 +464,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_270
         // touch at 0 degrees -> 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
         // touch at 90 degrees -> 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
         // touch at 180 degrees -> 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 270 degrees -> 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
     }
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index f210708..49c6fd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -414,7 +414,7 @@
         final float[] scaleFactor = new float[]{1f, displayHeight[1] / (float) displayHeight[0]};
         final int[] rotation = new int[]{Surface.ROTATION_0, Surface.ROTATION_90};
         final UdfpsOverlayParams oldParams = new UdfpsOverlayParams(sensorBounds[0],
-                displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]);
+                sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]);
 
         for (int i1 = 0; i1 <= 1; ++i1) {
             for (int i2 = 0; i2 <= 1; ++i2) {
@@ -422,8 +422,8 @@
                     for (int i4 = 0; i4 <= 1; ++i4) {
                         for (int i5 = 0; i5 <= 1; ++i5) {
                             final UdfpsOverlayParams newParams = new UdfpsOverlayParams(
-                                    sensorBounds[i1], displayWidth[i2], displayHeight[i3],
-                                    scaleFactor[i4], rotation[i5]);
+                                    sensorBounds[i1], sensorBounds[i1], displayWidth[i2],
+                                    displayHeight[i3], scaleFactor[i4], rotation[i5]);
 
                             if (newParams.equals(oldParams)) {
                                 continue;
@@ -466,8 +466,8 @@
 
         // Initialize the overlay.
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        rotation));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, rotation));
 
         // Show the overlay.
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
@@ -477,8 +477,8 @@
 
         // Update overlay with the same parameters.
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        rotation));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, rotation));
         mFgExecutor.runAllReady();
 
         // Ensure the overlay was not recreated.
@@ -525,8 +525,8 @@
 
         // Test ROTATION_0
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_0));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_0));
         MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor,
                 touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
@@ -542,8 +542,8 @@
         // Test ROTATION_90
         reset(mAlternateTouchProvider);
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_90));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_90));
         event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
@@ -558,8 +558,8 @@
         // Test ROTATION_270
         reset(mAlternateTouchProvider);
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_270));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_270));
         event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
@@ -574,8 +574,8 @@
         // Test ROTATION_180
         reset(mAlternateTouchProvider);
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_180));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_180));
         // ROTATION_180 is not supported. It should be treated like ROTATION_0.
         event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
@@ -665,7 +665,7 @@
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
         when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
         // WHEN it is cancelled
-        mUdfpsController.onCancelUdfps();
+        mUdfpsController.cancelAodInterrupt();
         // THEN the display is unconfigured
         verify(mUdfpsView).unconfigureDisplay();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index b78c063..ac936e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
@@ -68,7 +68,8 @@
         view = LayoutInflater.from(context).inflate(R.layout.udfps_view, null) as UdfpsView
         view.animationViewController = animationViewController
         val sensorBounds = SensorLocationInternal("", SENSOR_X, SENSOR_Y, SENSOR_RADIUS).rect
-        view.overlayParams = UdfpsOverlayParams(sensorBounds, 1920, 1080, 1f, Surface.ROTATION_0)
+        view.overlayParams = UdfpsOverlayParams(sensorBounds, sensorBounds, 1920,
+            1080, 1f, Surface.ROTATION_0)
         view.setUdfpsDisplayModeProvider(hbmProvider)
         ViewUtils.attachView(view)
     }
@@ -133,7 +134,8 @@
     @Test
     fun isNotWithinSensorArea() {
         whenever(animationViewController.touchTranslation).thenReturn(PointF(0f, 0f))
-        assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat())).isFalse()
+        assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat()))
+            .isFalse()
         assertThat(view.isWithinSensorArea(SENSOR_RADIUS.toFloat(), SENSOR_RADIUS * 2.5f)).isFalse()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
index 3e9cf1e..fa9c41a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
@@ -35,6 +35,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerFake;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -71,6 +72,8 @@
     @Mock
     private KeyguardStateController mKeyguardStateController;
     @Mock
+    private ShadeExpansionStateManager mShadeExpansionStateManager;
+    @Mock
     private BatteryController mBatteryController;
     private final DockManagerFake mDockManager = new DockManagerFake();
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
@@ -85,7 +88,8 @@
 
         mFalsingCollector = new FalsingCollectorImpl(mFalsingDataProvider, mFalsingManager,
                 mKeyguardUpdateMonitor, mHistoryTracker, mProximitySensor,
-                mStatusBarStateController, mKeyguardStateController, mBatteryController,
+                mStatusBarStateController, mKeyguardStateController, mShadeExpansionStateManager,
+                mBatteryController,
                 mDockManager, mFakeExecutor, mFakeSystemClock);
     }
 
@@ -137,9 +141,9 @@
     public void testUnregisterSensor_QS() {
         mFalsingCollector.onScreenTurningOn();
         reset(mProximitySensor);
-        mFalsingCollector.setQsExpanded(true);
+        mFalsingCollector.onQsExpansionChanged(true);
         verify(mProximitySensor).unregister(any(ThresholdSensor.Listener.class));
-        mFalsingCollector.setQsExpanded(false);
+        mFalsingCollector.onQsExpansionChanged(false);
         verify(mProximitySensor).register(any(ThresholdSensor.Listener.class));
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index d96ca91..677c7bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -40,6 +40,7 @@
 import com.android.systemui.screenshot.TimeoutHandler;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -47,6 +48,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+@Ignore("b/254635291")
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class ClipboardOverlayControllerTest extends SysuiTestCase {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
index 6a55a60..5bbd810 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.doze;
 
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+
 import static com.android.systemui.doze.DozeMachine.State.DOZE;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_DOCKED;
@@ -38,16 +41,17 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.UiModeManager;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.UiThreadTest;
 import android.view.Display;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -78,25 +82,30 @@
     @Mock
     private DozeHost mHost;
     @Mock
-    private UiModeManager mUiModeManager;
+    private DozeMachine.Part mPartMock;
+    @Mock
+    private DozeMachine.Part mAnotherPartMock;
     private DozeServiceFake mServiceFake;
     private WakeLockFake mWakeLockFake;
-    private AmbientDisplayConfiguration mConfigMock;
-    private DozeMachine.Part mPartMock;
+    private AmbientDisplayConfiguration mAmbientDisplayConfigMock;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mServiceFake = new DozeServiceFake();
         mWakeLockFake = new WakeLockFake();
-        mConfigMock = mock(AmbientDisplayConfiguration.class);
-        mPartMock = mock(DozeMachine.Part.class);
+        mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class);
         when(mDockManager.isDocked()).thenReturn(false);
         when(mDockManager.isHidden()).thenReturn(false);
 
-        mMachine = new DozeMachine(mServiceFake, mConfigMock, mWakeLockFake,
-                mWakefulnessLifecycle, mUiModeManager, mDozeLog, mDockManager,
-                mHost, new DozeMachine.Part[]{mPartMock});
+        mMachine = new DozeMachine(mServiceFake,
+                mAmbientDisplayConfigMock,
+                mWakeLockFake,
+                mWakefulnessLifecycle,
+                mDozeLog,
+                mDockManager,
+                mHost,
+                new DozeMachine.Part[]{mPartMock, mAnotherPartMock});
     }
 
     @Test
@@ -108,7 +117,7 @@
 
     @Test
     public void testInitialize_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
 
         mMachine.requestState(INITIALIZED);
 
@@ -118,7 +127,7 @@
 
     @Test
     public void testInitialize_goesToAod() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
 
         mMachine.requestState(INITIALIZED);
 
@@ -138,7 +147,7 @@
 
     @Test
     public void testInitialize_afterDockPaused_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -151,7 +160,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
 
         mMachine.requestState(INITIALIZED);
 
@@ -162,7 +171,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
 
         mMachine.requestState(INITIALIZED);
 
@@ -184,7 +193,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -197,7 +206,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -209,7 +218,7 @@
 
     @Test
     public void testPulseDone_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -222,7 +231,7 @@
 
     @Test
     public void testPulseDone_goesToAoD() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -236,7 +245,7 @@
     @Test
     public void testPulseDone_alwaysOnSuppressed_goesToSuppressed() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -287,7 +296,7 @@
 
     @Test
     public void testPulseDone_afterDockPaused_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
         mMachine.requestState(INITIALIZED);
@@ -303,7 +312,7 @@
     @Test
     public void testPulseDone_alwaysOnSuppressed_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
         mMachine.requestState(INITIALIZED);
@@ -471,7 +480,9 @@
 
     @Test
     public void testTransitionToInitialized_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
 
         verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED);
@@ -481,7 +492,9 @@
 
     @Test
     public void testTransitionToFinish_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(FINISH);
 
@@ -490,7 +503,9 @@
 
     @Test
     public void testDozeToDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE);
 
@@ -499,7 +514,9 @@
 
     @Test
     public void testDozeAoDToDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_AOD);
 
@@ -508,7 +525,9 @@
 
     @Test
     public void testDozePulsingBrightDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_PULSING_BRIGHT);
 
@@ -517,7 +536,9 @@
 
     @Test
     public void testDozeAodDockedDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_AOD_DOCKED);
 
@@ -525,7 +546,35 @@
     }
 
     @Test
+    public void testOnConfigurationChanged_propagatesUiModeTypeToParts() {
+        Configuration newConfig = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(newConfig);
+
+        verify(mPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mAnotherPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+    }
+
+    @Test
+    public void testOnConfigurationChanged_propagatesOnlyUiModeChangesToParts() {
+        Configuration newConfig = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(newConfig);
+        mMachine.onConfigurationChanged(newConfig);
+
+        verify(mPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mAnotherPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+    }
+
+    @Test
     public void testDozeSuppressTriggers_screenState() {
         assertEquals(Display.STATE_OFF, DOZE_SUSPEND_TRIGGERS.screenState(null));
     }
+
+    @NonNull
+    private Configuration configWithCarNightUiMode() {
+        Configuration configuration = Configuration.EMPTY;
+        configuration.uiMode = UI_MODE_TYPE_CAR | UI_MODE_NIGHT_YES;
+        return configuration;
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
index 0f29dcd..32b9945 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
@@ -10,14 +10,14 @@
  * 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 andatest
+ * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 package com.android.systemui.doze;
 
-import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
-import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+import static android.content.res.Configuration.UI_MODE_TYPE_NORMAL;
 
 import static com.android.systemui.doze.DozeMachine.State.DOZE;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
@@ -26,17 +26,16 @@
 import static com.android.systemui.doze.DozeMachine.State.INITIALIZED;
 import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED;
 
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.UiThreadTest;
@@ -44,13 +43,13 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.AdditionalMatchers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -71,10 +70,6 @@
     @Mock
     private AmbientDisplayConfiguration mConfig;
     @Mock
-    private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock
-    private UiModeManager mUiModeManager;
-    @Mock
     private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     @Mock
     private BiometricUnlockController mBiometricUnlockController;
@@ -83,13 +78,6 @@
     private DozeMachine mDozeMachine;
 
     @Captor
-    private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
-    @Captor
-    private ArgumentCaptor<IntentFilter> mIntentFilterCaptor;
-    private BroadcastReceiver mBroadcastReceiver;
-    private IntentFilter mIntentFilter;
-
-    @Captor
     private ArgumentCaptor<DozeHost.Callback> mDozeHostCaptor;
     private DozeHost.Callback mDozeHostCallback;
 
@@ -106,8 +94,6 @@
                 mDozeHost,
                 mConfig,
                 mDozeLog,
-                mBroadcastDispatcher,
-                mUiModeManager,
                 mBiometricUnlockControllerLazy);
 
         mDozeSuppressor.setDozeMachine(mDozeMachine);
@@ -122,36 +108,35 @@
     public void testRegistersListenersOnInitialized_unregisteredOnFinish() {
         // check that receivers and callbacks registered
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         captureDozeHostCallback();
 
         // check that receivers and callbacks are unregistered
         mDozeSuppressor.transitionTo(INITIALIZED, FINISH);
-        verify(mBroadcastDispatcher).unregisterReceiver(mBroadcastReceiver);
         verify(mDozeHost).removeCallback(mDozeHostCallback);
     }
 
     @Test
     public void testSuspendTriggersDoze_carMode() {
         // GIVEN car mode
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
 
         // WHEN dozing begins
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
 
         // THEN doze continues with all doze triggers disabled.
-        verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, atLeastOnce()).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, never())
+                .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS)));
     }
 
     @Test
     public void testSuspendTriggersDoze_enterCarMode() {
         // GIVEN currently dozing
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE);
 
         // WHEN car mode entered
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_ENTER_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
 
         // THEN doze continues with all doze triggers disabled.
         verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
@@ -160,13 +145,13 @@
     @Test
     public void testDozeResume_exitCarMode() {
         // GIVEN currently suspended, with AOD not enabled
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
         when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(false);
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
 
         // WHEN exiting car mode
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL);
 
         // THEN doze is resumed
         verify(mDozeMachine).requestState(DOZE);
@@ -175,19 +160,53 @@
     @Test
     public void testDozeAoDResume_exitCarMode() {
         // GIVEN currently suspended, with AOD not enabled
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
         when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(true);
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
 
         // WHEN exiting car mode
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL);
 
         // THEN doze AOD is resumed
         verify(mDozeMachine).requestState(DOZE_AOD);
     }
 
     @Test
+    public void testUiModeDoesNotChange_noStateTransition() {
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+        clearInvocations(mDozeMachine);
+
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+
+        verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, never())
+                .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS)));
+    }
+
+    @Test
+    public void testUiModeTypeChange_whenDozeMachineIsNotReady_doesNotDoAnything() {
+        when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true);
+
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+
+        verify(mDozeMachine, never()).requestState(any());
+    }
+
+    @Test
+    public void testUiModeTypeChange_CarModeEnabledAndDozeMachineNotReady_suspendsTriggersAfter() {
+        when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mDozeMachine, never()).requestState(any());
+
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+
+        verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS);
+    }
+
+    @Test
     public void testEndDoze_unprovisioned() {
         // GIVEN device unprovisioned
         when(mDozeHost.isProvisioned()).thenReturn(false);
@@ -276,14 +295,4 @@
         verify(mDozeHost).addCallback(mDozeHostCaptor.capture());
         mDozeHostCallback = mDozeHostCaptor.getValue();
     }
-
-    private void captureBroadcastReceiver() {
-        verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiverCaptor.capture(),
-                mIntentFilterCaptor.capture());
-        mBroadcastReceiver = mBroadcastReceiverCaptor.getValue();
-        mIntentFilter = mIntentFilterCaptor.getValue();
-        assertEquals(2, mIntentFilter.countActions());
-        org.hamcrest.MatcherAssert.assertThat(() -> mIntentFilter.actionsIterator(),
-                containsInAnyOrder(ACTION_ENTER_CAR_MODE, ACTION_EXIT_CAR_MODE));
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 781dc15..6091d3a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -23,10 +23,10 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -88,6 +88,8 @@
     @Mock
     private ProximityCheck mProximityCheck;
     @Mock
+    private DozeLog mDozeLog;
+    @Mock
     private AuthController mAuthController;
     @Mock
     private UiEventLogger mUiEventLogger;
@@ -127,7 +129,7 @@
 
         mTriggers = new DozeTriggers(mContext, mHost, config, dozeParameters,
                 asyncSensorManager, wakeLock, mDockManager, mProximitySensor,
-                mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings(),
+                mProximityCheck, mDozeLog, mBroadcastDispatcher, new FakeSettings(),
                 mAuthController, mUiEventLogger, mSessionTracker, mKeyguardStateController,
                 mDevicePostureController);
         mTriggers.setDozeMachine(mMachine);
@@ -342,6 +344,16 @@
         verify(mProximityCheck).destroy();
     }
 
+    @Test
+    public void testIsExecutingTransition_dropPulse() {
+        when(mHost.isPulsePending()).thenReturn(false);
+        when(mMachine.isExecutingTransition()).thenReturn(true);
+
+        mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_LONG_PRESS, 100, 100, null);
+
+        verify(mDozeLog).tracePulseDropped(anyString(), eq(null));
+    }
+
     private void waitForSensorManager() {
         mExecutor.runAllReady();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
index fc67201..65ae90b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -17,11 +17,19 @@
 package com.android.systemui.dump
 
 import androidx.test.filters.SmallTest
+import com.android.systemui.CoreStartable
 import com.android.systemui.Dumpable
+import com.android.systemui.ProtoDumpable
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.io.StringWriter
+import javax.inject.Provider
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
@@ -29,7 +37,6 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import java.io.PrintWriter
 
 @SmallTest
 class DumpHandlerTest : SysuiTestCase() {
@@ -43,6 +50,8 @@
 
     @Mock
     private lateinit var pw: PrintWriter
+    @Mock
+    private lateinit var fd: FileDescriptor
 
     @Mock
     private lateinit var dumpable1: Dumpable
@@ -52,6 +61,11 @@
     private lateinit var dumpable3: Dumpable
 
     @Mock
+    private lateinit var protoDumpable1: ProtoDumpable
+    @Mock
+    private lateinit var protoDumpable2: ProtoDumpable
+
+    @Mock
     private lateinit var buffer1: LogBuffer
     @Mock
     private lateinit var buffer2: LogBuffer
@@ -66,7 +80,9 @@
             mContext,
             dumpManager,
             logBufferEulogizer,
-            mutableMapOf(),
+            mutableMapOf(
+                EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() }
+            ),
             exceptionHandlerManager
         )
     }
@@ -82,7 +98,7 @@
 
         // WHEN some of them are dumped explicitly
         val args = arrayOf("dumpable1", "dumpable3", "buffer2")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN only the requested ones have their dump() method called
         verify(dumpable1).dump(pw, args)
@@ -101,7 +117,7 @@
 
         // WHEN that module is dumped
         val args = arrayOf("dumpable1")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN its dump() method is called
         verify(dumpable1).dump(pw, args)
@@ -118,7 +134,7 @@
 
         // WHEN a critical dump is requested
         val args = arrayOf("--dump-priority", "CRITICAL")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN all modules are dumped (but no buffers)
         verify(dumpable1).dump(pw, args)
@@ -139,7 +155,7 @@
 
         // WHEN a normal dump is requested
         val args = arrayOf("--dump-priority", "NORMAL")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN all buffers are dumped (but no modules)
         verify(dumpable1, never()).dump(
@@ -154,4 +170,44 @@
         verify(buffer1).dump(pw, 0)
         verify(buffer2).dump(pw, 0)
     }
-}
\ No newline at end of file
+
+    @Test
+    fun testConfigDump() {
+        // GIVEN a StringPrintWriter
+        val stringWriter = StringWriter()
+        val spw = PrintWriter(stringWriter)
+
+        // When a config dump is requested
+        dumpHandler.dump(fd, spw, arrayOf("config"))
+
+        assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName)
+    }
+
+    @Test
+    fun testDumpAllProtoDumpables() {
+        dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+        dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
+
+        val args = arrayOf(DumpHandler.PROTO)
+        dumpHandler.dump(fd, pw, args)
+
+        verify(protoDumpable1).dumpProto(any(), eq(args))
+        verify(protoDumpable2).dumpProto(any(), eq(args))
+    }
+
+    @Test
+    fun testDumpSingleProtoDumpable() {
+        dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+        dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
+
+        val args = arrayOf(DumpHandler.PROTO, "protoDumpable1")
+        dumpHandler.dump(fd, pw, args)
+
+        verify(protoDumpable1).dumpProto(any(), eq(args))
+        verify(protoDumpable2, never()).dumpProto(any(), any())
+    }
+
+    private class EmptyCoreStartable : CoreStartable {
+        override fun start() {}
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
index bd029a7..64547f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.dump
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogcatEchoTracker
 
 /**
  * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
new file mode 100644
index 0000000..1b34100
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -0,0 +1,259 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider
+import android.animation.ValueAnimator
+import android.util.Log
+import android.util.Log.TerribleFailure
+import android.util.Log.TerribleFailureHandler
+import android.view.Choreographer.FrameCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.truth.Truth.assertThat
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.UUID
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardTransitionRepository
+    private lateinit var oldWtfHandler: TerribleFailureHandler
+    private lateinit var wtfHandler: WtfHandler
+
+    @Before
+    fun setUp() {
+        underTest = KeyguardTransitionRepository()
+        wtfHandler = WtfHandler()
+        oldWtfHandler = Log.setWtfHandler(wtfHandler)
+    }
+
+    @After
+    fun tearDown() {
+        oldWtfHandler?.let { Log.setWtfHandler(it) }
+    }
+
+    @Test
+    fun `startTransition runs animator to completion`() =
+        runBlocking(IMMEDIATE) {
+            val (animator, provider) = setupAnimator(this)
+
+            val steps = mutableListOf<TransitionStep>()
+            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+            underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator))
+
+            val startTime = System.currentTimeMillis()
+            while (animator.isRunning()) {
+                yield()
+                if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
+                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
+                }
+            }
+
+            assertSteps(steps, listWithStep(BigDecimal(.1)))
+
+            job.cancel()
+            provider.stop()
+        }
+
+    @Test
+    fun `startTransition called during another transition fails`() {
+        underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null))
+        underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null))
+
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
+    @Test
+    fun `Null animator enables manual control with updateTransition`() =
+        runBlocking(IMMEDIATE) {
+            val steps = mutableListOf<TransitionStep>()
+            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+            val uuid =
+                underTest.startTransition(
+                    TransitionInfo(
+                        ownerName = OWNER_NAME,
+                        from = AOD,
+                        to = LOCKSCREEN,
+                        animator = null,
+                    )
+                )
+
+            checkNotNull(uuid).let {
+                underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+                underTest.updateTransition(it, 1f, TransitionState.FINISHED)
+            }
+
+            assertThat(steps.size).isEqualTo(3)
+            assertThat(steps[0])
+                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
+            assertThat(steps[1])
+                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING))
+            assertThat(steps[2])
+                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED))
+            job.cancel()
+        }
+
+    @Test
+    fun `Attempt to  manually update transition with invalid UUID throws exception`() {
+        underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
+    @Test
+    fun `Attempt to manually update transition after FINISHED state throws exception`() {
+        val uuid =
+            underTest.startTransition(
+                TransitionInfo(
+                    ownerName = OWNER_NAME,
+                    from = AOD,
+                    to = LOCKSCREEN,
+                    animator = null,
+                )
+            )
+
+        checkNotNull(uuid).let {
+            underTest.updateTransition(it, 1f, TransitionState.FINISHED)
+            underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+        }
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
+    private fun listWithStep(step: BigDecimal): List<BigDecimal> {
+        val steps = mutableListOf<BigDecimal>()
+
+        var i = BigDecimal.ZERO
+        while (i.compareTo(BigDecimal.ONE) <= 0) {
+            steps.add(i)
+            i = (i + step).setScale(2, RoundingMode.HALF_UP)
+        }
+
+        return steps
+    }
+
+    private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) {
+        // + 2 accounts for start and finish of automated transition
+        assertThat(steps.size).isEqualTo(fractions.size + 2)
+
+        assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
+        fractions.forEachIndexed { index, fraction ->
+            assertThat(steps[index + 1])
+                .isEqualTo(
+                    TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING)
+                )
+        }
+        assertThat(steps[steps.size - 1])
+            .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED))
+
+        assertThat(wtfHandler.failed).isFalse()
+    }
+
+    private fun setupAnimator(
+        scope: CoroutineScope
+    ): Pair<ValueAnimator, TestFrameCallbackProvider> {
+        val animator =
+            ValueAnimator().apply {
+                setInterpolator(Interpolators.LINEAR)
+                setDuration(ANIMATION_DURATION)
+            }
+
+        val provider = TestFrameCallbackProvider(animator, scope)
+        provider.start()
+
+        return Pair(animator, provider)
+    }
+
+    /** Gives direct control over ValueAnimator. See [AnimationHandler] */
+    private class TestFrameCallbackProvider(
+        private val animator: ValueAnimator,
+        private val scope: CoroutineScope,
+    ) : AnimationFrameCallbackProvider {
+
+        private var frameCount = 1L
+        private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
+        private var job: Job? = null
+
+        fun start() {
+            animator.getAnimationHandler().setProvider(this)
+
+            job =
+                scope.launch {
+                    frames.collect {
+                        // Delay is required for AnimationHandler to properly register a callback
+                        delay(1)
+                        val (frameNumber, callback) = it
+                        callback?.doFrame(frameNumber)
+                    }
+                }
+        }
+
+        fun stop() {
+            job?.cancel()
+            animator.getAnimationHandler().setProvider(null)
+        }
+
+        override fun postFrameCallback(cb: FrameCallback) {
+            frames.value = Pair(++frameCount, cb)
+        }
+        override fun postCommitCallback(runnable: Runnable) {}
+        override fun getFrameTime() = frameCount
+        override fun getFrameDelay() = 1L
+        override fun setFrameDelay(delay: Long) {}
+    }
+
+    private class WtfHandler : TerribleFailureHandler {
+        var failed = false
+        override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) {
+            failed = true
+        }
+    }
+
+    companion object {
+        private const val MAX_TEST_DURATION = 100L
+        private const val ANIMATION_DURATION = 10L
+        private const val OWNER_NAME = "Test"
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
index 1078cda..e009e86 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
@@ -19,9 +19,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 7c83cb7..6a4c0f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -22,6 +22,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -62,6 +65,34 @@
     }
 
     @Test
+    fun getIconFromPackageName_nullPackageName_returnsDefault() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger)
+
+        val expectedDesc =
+            ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name)
+                .loadContentDescription(context)
+        assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc)
+    }
+
+    @Test
+    fun getIconFromPackageName_invalidPackageName_returnsDefault() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger)
+
+        val expectedDesc =
+            ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name)
+                .loadContentDescription(context)
+        assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc)
+    }
+
+    @Test
+    fun getIconFromPackageName_validPackageName_returnsAppInfo() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger)
+
+        assertThat(icon)
+            .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME)))
+    }
+
+    @Test
     fun getIconInfoFromPackageName_nullPackageName_returnsDefault() {
         val iconInfo =
             MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index 110bbb8..fdeb3f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -17,14 +17,19 @@
 package com.android.systemui.media.taptotransfer.sender
 
 import android.app.StatusBarManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
 import android.media.MediaRoute2Info
 import android.os.PowerManager
+import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
+import android.widget.ImageView
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
@@ -32,16 +37,18 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.google.common.truth.Truth.assertThat
@@ -60,20 +67,29 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class MediaTttSenderCoordinatorTest : SysuiTestCase() {
+
+    // Note: This tests are a bit like integration tests because they use a real instance of
+    //   [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on
+    //   the inputs from [MediaTttSenderCoordinator].
+
     private lateinit var underTest: MediaTttSenderCoordinator
 
     @Mock private lateinit var accessibilityManager: AccessibilityManager
+    @Mock private lateinit var applicationInfo: ApplicationInfo
     @Mock private lateinit var commandQueue: CommandQueue
     @Mock private lateinit var configurationController: ConfigurationController
     @Mock private lateinit var falsingManager: FalsingManager
     @Mock private lateinit var falsingCollector: FalsingCollector
     @Mock private lateinit var logger: MediaTttLogger
     @Mock private lateinit var mediaTttFlags: MediaTttFlags
+    @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var powerManager: PowerManager
     @Mock private lateinit var viewUtil: ViewUtil
     @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var vibratorHelper: VibratorHelper
     private lateinit var chipbarCoordinator: ChipbarCoordinator
     private lateinit var commandQueueCallback: CommandQueue.Callbacks
+    private lateinit var fakeAppIconDrawable: Drawable
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
@@ -85,6 +101,18 @@
         whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
         whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
 
+        fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
+        whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
+        whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
+        whenever(
+                packageManager.getApplicationInfo(
+                    eq(PACKAGE_NAME),
+                    any<PackageManager.ApplicationInfoFlags>()
+                )
+            )
+            .thenReturn(applicationInfo)
+        context.setMockPackageManager(packageManager)
+
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
 
@@ -100,10 +128,10 @@
                 accessibilityManager,
                 configurationController,
                 powerManager,
-                uiEventLogger,
                 falsingManager,
                 falsingCollector,
                 viewUtil,
+                vibratorHelper,
             )
         chipbarCoordinator.start()
 
@@ -149,10 +177,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME))
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -163,10 +198,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME))
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -177,12 +219,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -193,12 +240,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -209,12 +261,66 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id)
+        verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_nullUndoCallback_noUndo() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ null
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
+        var undoCallbackCalled = false
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {
+                    undoCallbackCalled = true
+                }
+            },
+        )
+
+        getChipbarView().getUndoButton().performClick()
+
+        // Event index 1 since initially displaying the succeeded chip would also log an event
+        assertThat(uiEventLoggerFake.eventId(1))
+            .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id)
+        assertThat(undoCallbackCalled).isTrue()
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
     }
 
     @Test
@@ -225,12 +331,68 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id)
+        verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ null
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
+        var undoCallbackCalled = false
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {
+                    undoCallbackCalled = true
+                }
+            },
+        )
+
+        getChipbarView().getUndoButton().performClick()
+
+        // Event index 1 since initially displaying the succeeded chip would also log an event
+        assertThat(uiEventLoggerFake.eventId(1))
+            .isEqualTo(
+                MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id
+            )
+        assertThat(undoCallbackCalled).isTrue()
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
     }
 
     @Test
@@ -241,12 +403,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -257,12 +424,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -407,53 +579,113 @@
         verify(windowManager).removeView(any())
     }
 
-    private fun getChipView(): ViewGroup {
+    @Test
+    fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText())
+
+        // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should
+        // verify that the new state it triggers operates just like any other state.
+        getChipbarView().getUndoButton().performClick()
+        fakeExecutor.runAllReady()
+
+        // Verify that the click updated us to the triggered state
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        // Verify that we didn't remove the chipbar because it's in the triggered state
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+
+        // Verify we eventually remove the chipbar
+        verify(windowManager).removeView(any())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText())
+
+        // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should
+        // verify that the new state it triggers operates just like any other state.
+        getChipbarView().getUndoButton().performClick()
+        fakeExecutor.runAllReady()
+
+        // Verify that the click updated us to the triggered state
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        // Verify that we didn't remove the chipbar because it's in the triggered state
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+
+        // Verify we eventually remove the chipbar
+        verify(windowManager).removeView(any())
+    }
+
+    private fun getChipbarView(): ViewGroup {
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
         verify(windowManager).addView(viewCaptor.capture(), any())
         return viewCaptor.value as ViewGroup
     }
 
+    private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.start_icon)
+
     private fun ViewGroup.getChipText(): String =
         (this.requireViewById<TextView>(R.id.text)).text as String
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToStartCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo)
+    private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToEndCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo)
+    private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo)
+    private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
+    private fun ChipStateSender.getExpectedStateText(): String? {
+        return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context)
+    }
 }
 
+private const val APP_NAME = "Fake app name"
 private const val OTHER_DEVICE_NAME = "My Tablet"
+private const val PACKAGE_NAME = "com.android.systemui"
 private const val TIMEOUT = 10000
 
 private val routeInfo =
     MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
         .addFeature("feature")
-        .setClientPackageName("com.android.systemui")
+        .setClientPackageName(PACKAGE_NAME)
         .build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
index 0badd861..1bc4719 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
@@ -147,6 +147,18 @@
     }
 
     @Test
+    public void testMonochromatic() {
+        int colorInt = 0xffB3588A; // H350 C50 T50
+        ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
+                Style.MONOCHROMATIC /* style */);
+        int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+        Assert.assertTrue(
+                Color.red(neutralMid) == Color.green(neutralMid)
+                && Color.green(neutralMid) == Color.blue(neutralMid)
+        );
+    }
+
+    @Test
     @SuppressWarnings("ResultOfMethodCallIgnored")
     public void testToString() {
         new ColorScheme(Color.TRANSPARENT, false /* darkTheme */).toString();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
index aacc695..68c10f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
index 1c686c6..5e9c1aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
@@ -22,7 +22,6 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -52,6 +51,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.FrameLayout;
 import android.widget.TextView;
 
 import com.android.systemui.R;
@@ -97,6 +97,7 @@
     private static final int DEFAULT_ICON_ID = R.drawable.ic_info_outline;
 
     private ViewGroup mRootView;
+    private ViewGroup mSecurityFooterView;
     private TextView mFooterText;
     private TestableImageView mPrimaryFooterIcon;
     private QSSecurityFooter mFooter;
@@ -121,21 +122,26 @@
         Looper looper = mTestableLooper.getLooper();
         Handler mainHandler = new Handler(looper);
         when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class));
-        mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext)
+        mSecurityFooterView = (ViewGroup) new LayoutInflaterBuilder(mContext)
                 .replace("ImageView", TestableImageView.class)
                 .build().inflate(R.layout.quick_settings_security_footer, null, false);
         mFooterUtils = new QSSecurityFooterUtils(getContext(),
                 getContext().getSystemService(DevicePolicyManager.class), mUserTracker,
                 mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator);
-        mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper,
-                mBroadcastDispatcher, mFooterUtils);
-        mFooterText = mRootView.findViewById(R.id.footer_text);
-        mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon);
+        mFooter = new QSSecurityFooter(mSecurityFooterView, mainHandler, mSecurityController,
+                looper, mBroadcastDispatcher, mFooterUtils);
+        mFooterText = mSecurityFooterView.findViewById(R.id.footer_text);
+        mPrimaryFooterIcon = mSecurityFooterView.findViewById(R.id.primary_footer_icon);
 
         when(mSecurityController.getDeviceOwnerComponentOnAnyUser())
                 .thenReturn(DEVICE_OWNER_COMPONENT);
         when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
                 .thenReturn(DEVICE_OWNER_TYPE_DEFAULT);
+
+        // mSecurityFooterView must have a ViewGroup parent so that
+        // DialogLaunchAnimator.Controller.fromView() does not return null.
+        mRootView = new FrameLayout(mContext);
+        mRootView.addView(mSecurityFooterView);
         ViewUtils.attachView(mRootView);
 
         mFooter.init();
@@ -153,7 +159,7 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertEquals(View.GONE, mRootView.getVisibility());
+        assertEquals(View.GONE, mSecurityFooterView.getVisibility());
     }
 
     @Test
@@ -165,7 +171,7 @@
         TestableLooper.get(this).processAllMessages();
         assertEquals(mContext.getString(R.string.quick_settings_disclosure_management),
                      mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -181,7 +187,7 @@
         assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management,
                                         MANAGING_ORGANIZATION),
                 mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -200,7 +206,7 @@
         assertEquals(mContext.getString(
                 R.string.quick_settings_financed_disclosure_named_management,
                 MANAGING_ORGANIZATION), mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -217,7 +223,7 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertEquals(View.GONE, mRootView.getVisibility());
+        assertEquals(View.GONE, mSecurityFooterView.getVisibility());
     }
 
     @Test
@@ -227,8 +233,8 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertFalse(mRootView.isClickable());
-        assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertFalse(mSecurityFooterView.isClickable());
+        assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -241,8 +247,9 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertTrue(mRootView.isClickable());
-        assertEquals(View.VISIBLE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertTrue(mSecurityFooterView.isClickable());
+        assertEquals(View.VISIBLE,
+                mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -254,8 +261,8 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertFalse(mRootView.isClickable());
-        assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertFalse(mSecurityFooterView.isClickable());
+        assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -734,11 +741,11 @@
     @Test
     public void testDialogUsesDialogLauncher() {
         when(mSecurityController.isDeviceManaged()).thenReturn(true);
-        mFooter.onClick(mRootView);
+        mFooter.onClick(mSecurityFooterView);
 
         mTestableLooper.processAllMessages();
 
-        verify(mDialogLaunchAnimator).showFromView(any(), eq(mRootView), any());
+        verify(mDialogLaunchAnimator).show(any(), any());
     }
 
     @Test
@@ -775,7 +782,7 @@
         ArgumentCaptor<AlertDialog> dialogCaptor = ArgumentCaptor.forClass(AlertDialog.class);
 
         mTestableLooper.processAllMessages();
-        verify(mDialogLaunchAnimator).showFromView(dialogCaptor.capture(), any(), any());
+        verify(mDialogLaunchAnimator).show(dialogCaptor.capture(), any());
 
         AlertDialog dialog = dialogCaptor.getValue();
         dialog.create();
@@ -817,8 +824,8 @@
         verify(mBroadcastDispatcher).registerReceiverWithHandler(captor.capture(), any(), any(),
                 any());
 
-        // Pretend view is not visible temporarily
-        mRootView.onVisibilityAggregated(false);
+        // Pretend view is not attached anymore.
+        mRootView.removeView(mSecurityFooterView);
         captor.getValue().onReceive(mContext,
                 new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG));
         mTestableLooper.processAllMessages();
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 3c58b6fc..c452872 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.nano.SystemUIProtoDump;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
@@ -114,8 +115,6 @@
     @Mock
     private DumpManager mDumpManager;
     @Mock
-    private QSTile.State mMockState;
-    @Mock
     private CentralSurfaces mCentralSurfaces;
     @Mock
     private QSLogger mQSLogger;
@@ -195,7 +194,6 @@
     }
 
     private void setUpTileFactory() {
-        when(mMockState.toString()).thenReturn(MOCK_STATE_STRING);
         // Only create this kind of tiles
         when(mDefaultFactory.createTile(anyString())).thenAnswer(
                 invocation -> {
@@ -209,7 +207,11 @@
                     } else if ("na".equals(spec)) {
                         return new NotAvailableTile(mQSTileHost);
                     } else if (CUSTOM_TILE_SPEC.equals(spec)) {
-                        return mCustomTile;
+                        QSTile tile = mCustomTile;
+                        QSTile.State s = mock(QSTile.State.class);
+                        s.spec = spec;
+                        when(mCustomTile.getState()).thenReturn(s);
+                        return tile;
                     } else if ("internet".equals(spec)
                             || "wifi".equals(spec)
                             || "cell".equals(spec)) {
@@ -647,7 +649,7 @@
     @Test
     public void testSetTileRemoved_removedBySystem() {
         int user = mUserTracker.getUserId();
-        saveSetting("spec1" + CUSTOM_TILE_SPEC);
+        saveSetting("spec1," + CUSTOM_TILE_SPEC);
 
         // This will be done by TileServiceManager
         mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
@@ -658,6 +660,27 @@
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
+    @Test
+    public void testProtoDump_noTiles() {
+        SystemUIProtoDump proto = new SystemUIProtoDump();
+        mQSTileHost.dumpProto(proto, new String[0]);
+
+        assertEquals(0, proto.tiles.length);
+    }
+
+    @Test
+    public void testTilesInOrder() {
+        saveSetting("spec1," + CUSTOM_TILE_SPEC);
+
+        SystemUIProtoDump proto = new SystemUIProtoDump();
+        mQSTileHost.dumpProto(proto, new String[0]);
+
+        assertEquals(2, proto.tiles.length);
+        assertEquals("spec1", proto.tiles[0].getSpec());
+        assertEquals(CUSTOM_TILE.getPackageName(), proto.tiles[1].getComponentName().packageName);
+        assertEquals(CUSTOM_TILE.getClassName(), proto.tiles[1].getComponentName().className);
+    }
+
     private SharedPreferences getSharedPreferenecesForUser(int user) {
         return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user);
     }
@@ -707,12 +730,9 @@
 
         @Override
         public State newTileState() {
-            return mMockState;
-        }
-
-        @Override
-        public State getState() {
-            return mMockState;
+            State s = mock(QSTile.State.class);
+            when(s.toString()).thenReturn(MOCK_STATE_STRING);
+            return s;
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt
new file mode 100644
index 0000000..629c663
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt
@@ -0,0 +1,104 @@
+package com.android.systemui.qs
+
+import android.content.ComponentName
+import android.service.quicksettings.Tile
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.external.CustomTile
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TileStateToProtoTest : SysuiTestCase() {
+
+    companion object {
+        private const val TEST_LABEL = "label"
+        private const val TEST_SUBTITLE = "subtitle"
+        private const val TEST_SPEC = "spec"
+        private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls")
+    }
+
+    @Test
+    fun platformTile_INACTIVE() {
+        val state =
+            QSTile.State().apply {
+                spec = TEST_SPEC
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_INACTIVE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isTrue()
+        assertThat(proto?.spec).isEqualTo(TEST_SPEC)
+        assertThat(proto?.hasComponentName()).isFalse()
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_INACTIVE)
+        assertThat(proto?.hasBooleanState()).isFalse()
+    }
+
+    @Test
+    fun componentTile_UNAVAILABLE() {
+        val state =
+            QSTile.State().apply {
+                spec = CustomTile.toSpec(TEST_COMPONENT)
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_UNAVAILABLE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isFalse()
+        assertThat(proto?.hasComponentName()).isTrue()
+        val componentName = proto?.componentName
+        assertThat(componentName?.packageName).isEqualTo(TEST_COMPONENT.packageName)
+        assertThat(componentName?.className).isEqualTo(TEST_COMPONENT.className)
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_UNAVAILABLE)
+        assertThat(proto?.hasBooleanState()).isFalse()
+    }
+
+    @Test
+    fun booleanState_ACTIVE() {
+        val state =
+            QSTile.BooleanState().apply {
+                spec = TEST_SPEC
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_ACTIVE
+                value = true
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isTrue()
+        assertThat(proto?.spec).isEqualTo(TEST_SPEC)
+        assertThat(proto?.hasComponentName()).isFalse()
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_ACTIVE)
+        assertThat(proto?.hasBooleanState()).isTrue()
+        assertThat(proto?.booleanState).isTrue()
+    }
+
+    @Test
+    fun noSpec_returnsNull() {
+        val state =
+            QSTile.State().apply {
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_ACTIVE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
index 3c25807..2c2ddbb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
@@ -23,13 +23,13 @@
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.nano.MetricsProto
 import com.android.internal.logging.testing.FakeMetricsLogger
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.globalactions.GlobalActionsDialogLite
@@ -70,13 +70,13 @@
         val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils)
 
         val quickSettingsContext = mock<Context>()
-        underTest.showDeviceMonitoringDialog(quickSettingsContext)
+
+        underTest.showDeviceMonitoringDialog(quickSettingsContext, null)
         verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
 
-        val view = mock<View>()
-        whenever(view.context).thenReturn(quickSettingsContext)
-        underTest.showDeviceMonitoringDialog(view)
-        verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
+        val expandable = mock<Expandable>()
+        underTest.showDeviceMonitoringDialog(quickSettingsContext, expandable)
+        verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, expandable)
     }
 
     @Test
@@ -85,8 +85,8 @@
         val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger)
 
         val globalActionsDialogLite = mock<GlobalActionsDialogLite>()
-        val view = mock<View>()
-        underTest.showPowerMenuDialog(globalActionsDialogLite, view)
+        val expandable = mock<Expandable>()
+        underTest.showPowerMenuDialog(globalActionsDialogLite, expandable)
 
         // Event is logged.
         val logs = uiEventLogger.logs
@@ -99,7 +99,7 @@
             .showOrHideDialog(
                 /* keyguardShowing= */ false,
                 /* isDeviceProvisioned= */ true,
-                view,
+                expandable,
             )
     }
 
@@ -167,11 +167,11 @@
                 userSwitchDialogController = userSwitchDialogController,
             )
 
-        val view = mock<View>()
-        underTest.showUserSwitcher(view)
+        val expandable = mock<Expandable>()
+        underTest.showUserSwitcher(context, expandable)
 
         // Dialog is shown.
-        verify(userSwitchDialogController).showDialog(view)
+        verify(userSwitchDialogController).showDialog(context, expandable)
     }
 
     @Test
@@ -184,12 +184,9 @@
                 activityStarter = activityStarter,
             )
 
-        // The clicked view. The context is necessary because it's used to build the intent, that
-        // we check below.
-        val view = mock<View>()
-        whenever(view.context).thenReturn(context)
-
-        underTest.showUserSwitcher(view)
+        // The clicked expandable.
+        val expandable = mock<Expandable>()
+        underTest.showUserSwitcher(context, expandable)
 
         // Dialog is shown.
         val intentCaptor = argumentCaptor<Intent>()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index da52a9b..bc27bbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.user.data.source.UserRecord
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -139,6 +140,11 @@
         clickableTest(false, false, mUserDetailItemView, true)
     }
 
+    @Test
+    fun testManageUsersIsNotAvailable() {
+        assertNull(adapter.users.find { it.isManageUsers })
+    }
+
     private fun createUserRecord(current: Boolean, guest: Boolean) =
         UserRecord(
             UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 9d908fd..0a34810 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -20,12 +20,12 @@
 import android.content.Intent
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
-import android.view.View
 import android.widget.Button
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.PseudoGridView
@@ -35,6 +35,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -63,7 +64,7 @@
     @Mock
     private lateinit var userDetailViewAdapter: UserDetailView.Adapter
     @Mock
-    private lateinit var launchView: View
+    private lateinit var launchExpandable: Expandable
     @Mock
     private lateinit var neutralButton: Button
     @Mock
@@ -79,7 +80,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(launchView.context).thenReturn(mContext)
         `when`(dialog.context).thenReturn(mContext)
 
         controller = UserSwitchDialogController(
@@ -94,32 +94,34 @@
 
     @Test
     fun showDialog_callsDialogShow() {
-        controller.showDialog(launchView)
-        verify(dialogLaunchAnimator).showFromView(eq(dialog), eq(launchView), any(), anyBoolean())
+        val launchController = mock<DialogLaunchAnimator.Controller>()
+        `when`(launchExpandable.dialogLaunchController(any())).thenReturn(launchController)
+        controller.showDialog(context, launchExpandable)
+        verify(dialogLaunchAnimator).show(eq(dialog), eq(launchController), anyBoolean())
         verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN)
     }
 
     @Test
     fun dialog_showForAllUsers() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(dialog).setShowForAllUsers(true)
     }
 
     @Test
     fun dialog_cancelOnTouchOutside() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(dialog).setCanceledOnTouchOutside(true)
     }
 
     @Test
     fun adapterAndGridLinked() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(userDetailViewAdapter).linkToViewGroup(any<PseudoGridView>())
     }
 
     @Test
     fun doneButtonLogsCorrectly() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog).setPositiveButton(anyInt(), capture(clickCaptor))
 
@@ -132,7 +134,7 @@
     fun clickSettingsButton_noFalsing_opensSettings() {
         `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false)
 
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog)
             .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
@@ -153,7 +155,7 @@
     fun clickSettingsButton_Falsing_notOpensSettings() {
         `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true)
 
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog)
             .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index 0151822..14a3bc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -659,6 +659,51 @@
         verify(privacyIconsController, never()).onParentInvisible()
     }
 
+    @Test
+    fun clockPivotYInCenter() {
+        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+        verify(clock).addOnLayoutChangeListener(capture(captor))
+        var height = 100
+        val width = 50
+
+        clock.executeLayoutChange(0, 0, width, height, captor.value)
+        verify(clock).pivotY = height.toFloat() / 2
+
+        height = 150
+        clock.executeLayoutChange(0, 0, width, height, captor.value)
+        verify(clock).pivotY = height.toFloat() / 2
+    }
+
+    private fun View.executeLayoutChange(
+            left: Int,
+            top: Int,
+            right: Int,
+            bottom: Int,
+            listener: View.OnLayoutChangeListener
+    ) {
+        val oldLeft = this.left
+        val oldTop = this.top
+        val oldRight = this.right
+        val oldBottom = this.bottom
+        whenever(this.left).thenReturn(left)
+        whenever(this.top).thenReturn(top)
+        whenever(this.right).thenReturn(right)
+        whenever(this.bottom).thenReturn(bottom)
+        whenever(this.height).thenReturn(bottom - top)
+        whenever(this.width).thenReturn(right - left)
+        listener.onLayoutChange(
+                this,
+                oldLeft,
+                oldTop,
+                oldRight,
+                oldBottom,
+                left,
+                top,
+                right,
+                bottom
+        )
+    }
+
     private fun createWindowInsets(
         topCutout: Rect? = Rect()
     ): WindowInsets {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index c0dae03..e444a39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -18,6 +18,7 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
+import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -33,11 +34,13 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -76,6 +79,7 @@
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.FaceAuthApiRequestReason;
 import com.android.keyguard.KeyguardClockSwitch;
 import com.android.keyguard.KeyguardClockSwitchController;
 import com.android.keyguard.KeyguardStatusView;
@@ -93,7 +97,6 @@
 import com.android.systemui.camera.CameraGestureHelper;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
@@ -106,10 +109,11 @@
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -165,7 +169,6 @@
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.time.SystemClock;
-import com.android.systemui.wallet.controller.QuickAccessWalletController;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
 import org.junit.After;
@@ -173,6 +176,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
@@ -251,17 +256,15 @@
     @Mock private KeyguardMediaController mKeyguardMediaController;
     @Mock private PrivacyDotViewController mPrivacyDotViewController;
     @Mock private NavigationModeController mNavigationModeController;
+    @Mock private NavigationBarController mNavigationBarController;
     @Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
     @Mock private ContentResolver mContentResolver;
     @Mock private TapAgainViewController mTapAgainViewController;
     @Mock private KeyguardIndicationController mKeyguardIndicationController;
     @Mock private FragmentService mFragmentService;
     @Mock private FragmentHostManager mFragmentHostManager;
-    @Mock private QuickAccessWalletController mQuickAccessWalletController;
-    @Mock private QRCodeScannerController mQrCodeScannerController;
     @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
     @Mock private RecordingController mRecordingController;
-    @Mock private ControlsComponent mControlsComponent;
     @Mock private LockscreenGestureLogger mLockscreenGestureLogger;
     @Mock private DumpManager mDumpManager;
     @Mock private InteractionJankMonitor mInteractionJankMonitor;
@@ -282,6 +285,10 @@
     @Mock private ViewTreeObserver mViewTreeObserver;
     @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+    @Mock private MotionEvent mDownMotionEvent;
+    @Captor
+    private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
+            mEmptySpaceClickListenerCaptor;
 
     private NotificationPanelViewController.TouchHandler mTouchHandler;
     private ConfigurationController mConfigurationController;
@@ -373,6 +380,7 @@
 
         NotificationWakeUpCoordinator coordinator =
                 new NotificationWakeUpCoordinator(
+                        mDumpManager,
                         mock(HeadsUpManagerPhone.class),
                         new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
                                 mInteractionJankMonitor),
@@ -388,6 +396,7 @@
                 mConfigurationController,
                 mStatusBarStateController,
                 mFalsingManager,
+                mShadeExpansionStateManager,
                 mLockscreenShadeTransitionController,
                 new FalsingCollectorFake(),
                 mDumpManager);
@@ -425,6 +434,8 @@
         when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
         when(mView.getParent()).thenReturn(mViewParent);
         when(mQs.getHeader()).thenReturn(mQsHeader);
+        when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
+        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
 
         mMainHandler = new Handler(Looper.getMainLooper());
         NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter =
@@ -468,6 +479,7 @@
                 mPrivacyDotViewController,
                 mTapAgainViewController,
                 mNavigationModeController,
+                mNavigationBarController,
                 mFragmentService,
                 mContentResolver,
                 mRecordingController,
@@ -512,6 +524,8 @@
                 .addCallback(mNotificationPanelViewController.mStatusBarStateListener);
         mNotificationPanelViewController
                 .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+        verify(mNotificationStackScrollLayoutController)
+                .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
     }
 
     @After
@@ -750,6 +764,38 @@
     }
 
     @Test
+    public void testOnTouchEvent_expansionResumesAfterBriefTouch() {
+        // Start shade collapse with swipe up
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
+                0 /* metaState */));
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        assertThat(mNotificationPanelViewController.getClosing()).isTrue();
+        assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue();
+
+        // simulate touch that does not exceed touch slop
+        onTouchEvent(MotionEvent.obtain(2L /* downTime */,
+                2L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        mNotificationPanelViewController.setTouchSlopExceeded(false);
+
+        onTouchEvent(MotionEvent.obtain(2L /* downTime */,
+                2L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        // fling should still be called after a touch that does not exceed touch slop
+        assertThat(mNotificationPanelViewController.getClosing()).isTrue();
+        assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue();
+    }
+
+    @Test
     public void handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() {
         when(mCommandQueue.panelsEnabled()).thenReturn(false);
 
@@ -1540,6 +1586,103 @@
         );
     }
 
+    @Test
+    public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, false);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor).requestFaceAuth(true,
+                FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_notDozingAndFaceDetectionIsNotRunning_startsUnlockAnimation() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, false);
+        when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(false);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mNotificationStackScrollLayoutController).setUnlockHintRunning(true);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_notDozingAndFaceDetectionIsRunning_doesNotStartUnlockHint() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, false);
+        when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(true);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mNotificationStackScrollLayoutController, never()).setUnlockHintRunning(true);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_whenDozingAndOnKeyguard_doesNotRequestFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(true, false);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString());
+    }
+
+    @Test
+    public void onEmptySpaceClicked_whenStatusBarShadeLocked_doesNotRequestFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(SHADE_LOCKED);
+
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString());
+
+    }
+
+    /**
+     * When shade is flinging to close and this fling is not intercepted,
+     * {@link AmbientState#setIsClosing(boolean)} should be called before
+     * {@link NotificationStackScrollLayoutController#onExpansionStopped()}
+     * to ensure scrollY can be correctly set to be 0
+     */
+    @Test
+    public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() {
+        // Given: Shade is expanded
+        mNotificationPanelViewController.notifyExpandingFinished();
+        mNotificationPanelViewController.setIsClosing(false);
+
+        // When: Shade flings to close not canceled
+        mNotificationPanelViewController.notifyExpandingStarted();
+        mNotificationPanelViewController.setIsClosing(true);
+        mNotificationPanelViewController.onFlingEnd(false);
+
+        // Then: AmbientState's mIsClosing should be set to false
+        // before mNotificationStackScrollLayoutController.onExpansionStopped() is called
+        // to ensure NotificationStackScrollLayout.resetScrollPosition() -> resetScrollPosition
+        // -> setOwnScrollY(0) can set scrollY to 0 when shade is closed
+        InOrder inOrder = inOrder(mAmbientState, mNotificationStackScrollLayoutController);
+        inOrder.verify(mAmbientState).setIsClosing(false);
+        inOrder.verify(mNotificationStackScrollLayoutController).onExpansionStopped();
+    }
+
     private static MotionEvent createMotionEvent(int x, int y, int action) {
         return MotionEvent.obtain(
                 /* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
index 12ef036..bdafc7d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
@@ -66,6 +66,8 @@
     @Mock
     private lateinit var largeScreenShadeHeaderController: LargeScreenShadeHeaderController
     @Mock
+    private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+    @Mock
     private lateinit var featureFlags: FeatureFlags
     @Captor
     lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
@@ -96,6 +98,7 @@
                 navigationModeController,
                 overviewProxyService,
                 largeScreenShadeHeaderController,
+                shadeExpansionStateManager,
                 featureFlags,
                 delayableExecutor
         )
@@ -380,6 +383,7 @@
                 navigationModeController,
                 overviewProxyService,
                 largeScreenShadeHeaderController,
+                shadeExpansionStateManager,
                 featureFlags,
                 delayableExecutor
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index ad3d3d2..95cf9d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -88,6 +88,7 @@
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock private AuthController mAuthController;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
 
     private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
@@ -103,7 +104,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager, mKeyguardStateController,
-                mScreenOffAnimationController, mAuthController) {
+                mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager) {
                     @Override
                     protected boolean isDebuggable() {
                         return false;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
index eb34561..cc45cf88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.TextAnimator
+import com.android.systemui.util.mockito.any
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -55,7 +56,7 @@
         clockView.animateAppearOnLockscreen()
         clockView.measure(50, 50)
 
-        verify(mockTextAnimator).glyphFilter = null
+        verify(mockTextAnimator).glyphFilter = any()
         verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null)
         verifyNoMoreInteractions(mockTextAnimator)
     }
@@ -66,7 +67,7 @@
         clockView.measure(50, 50)
         clockView.animateAppearOnLockscreen()
 
-        verify(mockTextAnimator, times(2)).glyphFilter = null
+        verify(mockTextAnimator, times(2)).glyphFilter = any()
         verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null)
         verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null)
         verifyNoMoreInteractions(mockTextAnimator)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
index 5b34a95..b761647 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
@@ -17,58 +17,58 @@
 
 @SmallTest
 class UncaughtExceptionPreHandlerTest : SysuiTestCase() {
-  private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
+    private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
 
-  @Mock private lateinit var mockHandler: UncaughtExceptionHandler
+    @Mock private lateinit var mockHandler: UncaughtExceptionHandler
 
-  @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
+    @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
 
-  @Before
-  fun setUp() {
-    MockitoAnnotations.initMocks(this)
-    Thread.setUncaughtExceptionPreHandler(null)
-    preHandlerManager = UncaughtExceptionPreHandlerManager()
-  }
-
-  @Test
-  fun registerHandler_registersOnceOnly() {
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  fun registerHandler_setsUncaughtExceptionPreHandler() {
-    Thread.setUncaughtExceptionPreHandler(null)
-    preHandlerManager.registerHandler(mockHandler)
-    assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
-  }
-
-  @Test
-  fun registerHandler_preservesOriginalHandler() {
-    Thread.setUncaughtExceptionPreHandler(mockHandler)
-    preHandlerManager.registerHandler(mockHandler2)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  @Ignore
-  fun registerHandler_toleratesHandlersThatThrow() {
-    `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
-    preHandlerManager.registerHandler(mockHandler2)
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler2, only()).uncaughtException(any(), any())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  fun registerHandler_doesNotSetUpTwice() {
-    UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
-    assertThrows(IllegalStateException::class.java) {
-      preHandlerManager.registerHandler(mockHandler)
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        Thread.setUncaughtExceptionPreHandler(null)
+        preHandlerManager = UncaughtExceptionPreHandlerManager()
     }
-  }
+
+    @Test
+    fun registerHandler_registersOnceOnly() {
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    fun registerHandler_setsUncaughtExceptionPreHandler() {
+        Thread.setUncaughtExceptionPreHandler(null)
+        preHandlerManager.registerHandler(mockHandler)
+        assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
+    }
+
+    @Test
+    fun registerHandler_preservesOriginalHandler() {
+        Thread.setUncaughtExceptionPreHandler(mockHandler)
+        preHandlerManager.registerHandler(mockHandler2)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    @Ignore
+    fun registerHandler_toleratesHandlersThatThrow() {
+        `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
+        preHandlerManager.registerHandler(mockHandler2)
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler2, only()).uncaughtException(any(), any())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    fun registerHandler_doesNotSetUpTwice() {
+        UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
+        assertThrows(IllegalStateException::class.java) {
+            preHandlerManager.registerHandler(mockHandler)
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
index 8cb530c..5fc0ffe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
@@ -4,7 +4,7 @@
 import android.util.DisplayMetrics
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
index 44cbe51..fbb8ebf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager
@@ -56,6 +57,7 @@
     private val configurationController: ConfigurationController = mock()
     private val statusBarStateController: StatusBarStateController = mock()
     private val falsingManager: FalsingManager = mock()
+    private val shadeExpansionStateManager: ShadeExpansionStateManager = mock()
     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock()
     private val falsingCollector: FalsingCollector = mock()
     private val dumpManager: DumpManager = mock()
@@ -65,7 +67,8 @@
     fun setUp() {
         whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight)
 
-        pulseExpansionHandler = PulseExpansionHandler(
+        pulseExpansionHandler =
+            PulseExpansionHandler(
                 mContext,
                 wakeUpCoordinator,
                 bypassController,
@@ -74,10 +77,11 @@
                 configurationController,
                 statusBarStateController,
                 falsingManager,
+                shadeExpansionStateManager,
                 lockscreenShadeTransitionController,
                 falsingCollector,
                 dumpManager
-        )
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
index f8a0d2f..9c65fac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
@@ -70,7 +70,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
 import com.android.systemui.telephony.TelephonyListenerManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
index ed8a3e1..4bed4a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
@@ -38,7 +38,7 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
index a76676e..d5f5105 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
@@ -43,7 +43,7 @@
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
index 4b458f5..dda7fad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
@@ -31,8 +31,8 @@
     private long mCreationTime = 0;
     @Nullable private GroupEntry mParent = GroupEntry.ROOT_ENTRY;
     private NotifSection mNotifSection;
-    private NotificationEntry mSummary = null;
-    private List<NotificationEntry> mChildren = new ArrayList<>();
+    @Nullable private NotificationEntry mSummary = null;
+    private final List<NotificationEntry> mChildren = new ArrayList<>();
 
     /** Builds a new instance of GroupEntry */
     public GroupEntry build() {
@@ -41,7 +41,9 @@
         ge.getAttachState().setSection(mNotifSection);
 
         ge.setSummary(mSummary);
-        mSummary.setParent(ge);
+        if (mSummary != null) {
+            mSummary.setParent(ge);
+        }
 
         for (NotificationEntry child : mChildren) {
             ge.addChild(child);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 851517e..3b05321 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -1498,45 +1498,8 @@
     }
 
     @Test
-    public void testMissingRankingWhenRemovalFeatureIsDisabled() {
+    public void testMissingRanking() {
         // GIVEN a pipeline with one two notifications
-        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false);
-        String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
-        String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
-        NotificationEntry entry1 = mCollectionListener.getEntry(key1);
-        NotificationEntry entry2 = mCollectionListener.getEntry(key2);
-        clearInvocations(mCollectionListener);
-
-        // GIVEN the message for removing key1 gets does not reach NotifCollection
-        Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
-        // WHEN the message for removing key2 arrives
-        mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
-
-        // THEN only entry2 gets removed
-        verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
-        verify(mCollectionListener).onEntryCleanUp(eq(entry2));
-        verify(mCollectionListener).onRankingApplied();
-        verifyNoMoreInteractions(mCollectionListener);
-        verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
-        verify(mLogger, never()).logRecoveredRankings(any(), anyInt());
-        clearInvocations(mCollectionListener, mLogger);
-
-        // WHEN a ranking update includes key1 again
-        mNoMan.setRanking(key1, ranking1);
-        mNoMan.issueRankingUpdate();
-
-        // VERIFY that we do nothing but log the 'recovery'
-        verify(mCollectionListener).onRankingUpdate(any());
-        verify(mCollectionListener).onRankingApplied();
-        verifyNoMoreInteractions(mCollectionListener);
-        verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
-        verify(mLogger).logRecoveredRankings(eq(List.of(key1)), eq(0));
-    }
-
-    @Test
-    public void testMissingRankingWhenRemovalFeatureIsEnabled() {
-        // GIVEN a pipeline with one two notifications
-        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true);
         String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
         String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
         NotificationEntry entry1 = mCollectionListener.getEntry(key1);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 82e32b2..09f8a10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -34,10 +34,12 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
@@ -135,6 +137,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         allowTestableLooperAsMainThread();
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
 
         mListBuilder = new ShadeListBuilder(
                 mDumpManager,
@@ -1995,22 +1998,89 @@
     }
 
     @Test
-    public void testStableOrdering() {
+    public void testActiveOrdering_withLegacyStability() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
+    }
+
+    @Test
+    public void testStableOrdering_withLegacyStability() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
         mStabilityManager.setAllowEntryReordering(false);
-        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap
-        verify(mStabilityManager, times(4)).onEntryReorderSuppressed();
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap
+    }
+
+    @Test
+    public void testStableOrdering() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        mStabilityManager.setAllowEntryReordering(false);
+        // No input or output
+        assertOrder("", "", "", true);
+        // Remove everything
+        assertOrder("ABCDEFG", "", "", true);
+        // Literally no changes
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true);
+
+        // No stable order
+        assertOrder("", "ABCDEFG", "ABCDEFG", true);
+
+        // F moved after A, and...
+        assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F
+        assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F
+        assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was
+
+        // B moved after F, and...
+        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B
+        assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B
+        assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was
+
+        // Swap F and B, and...
+        assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F
+        assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F
+        assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG)
+        assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG)
+        assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B
+        assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B
+
+        // Remove a bunch of entries at once
+        assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true);
+
+        // Remove a bunch of entries and scramble
+        assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false);
+
+        // Add a bunch of entries at once
+        assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true);
+
+        // Add a bunch of entries and reverse originals
+        // NOTE: Some of these don't have obviously correct answers
+        assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended
+        assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended
+        assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append
+        assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend
+        assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries
+
+        // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+        assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false);
     }
 
     @Test
     public void testActiveOrdering() {
-        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap
-        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
     }
 
     @Test
@@ -2062,6 +2132,52 @@
     }
 
     @Test
+    public void stableOrderingDisregardedWithSectionChange() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        // GIVEN the first sectioner's packages can be changed from run-to-run
+        List<String> mutableSectionerPackages = new ArrayList<>();
+        mutableSectionerPackages.add(PACKAGE_1);
+        mListBuilder.setSectioners(asList(
+                new PackageSectioner(mutableSectionerPackages, null),
+                new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null)));
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(4);
+        addNotif(1, PACKAGE_1).setRank(5);
+        addNotif(2, PACKAGE_2).setRank(1);
+        addNotif(3, PACKAGE_2).setRank(2);
+        addNotif(4, PACKAGE_3).setRank(3);
+        dispatchBuild();
+
+        // VERIFY the order and that entry reordering has not been suppressed
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3),
+                notif(4)
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+        // WHEN the first section now claims PACKAGE_3 notifications
+        mutableSectionerPackages.add(PACKAGE_3);
+        dispatchBuild();
+
+        // VERIFY the re-sectioned notification is inserted at #1 of the first section, which
+        // is the correct position based on its rank, rather than #3 in the new section simply
+        // because it was #3 in its previous section.
+        verifyBuiltList(
+                notif(4),
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3)
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+    }
+
+    @Test
     public void testStableChildOrdering() {
         // WHEN the list is originally built with reordering disabled
         mStabilityManager.setAllowEntryReordering(false);
@@ -2112,6 +2228,85 @@
         );
     }
 
+    @Test
+    public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() {
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false);
+
+        // GIVEN a notification group is on screen
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(2);
+        addNotif(1, PACKAGE_1).setRank(3);
+        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+        addGroupChild(3, PACKAGE_1, "group").setRank(5);
+        addGroupChild(4, PACKAGE_1, "group").setRank(6);
+        dispatchBuild();
+
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                group(
+                        summary(2),
+                        child(3),
+                        child(4)
+                )
+        );
+
+        // WHEN the notification summary rank increases and children removed
+        setNewRank(notif(2).entry, 1);
+        mEntrySet.remove(4);
+        mEntrySet.remove(3);
+        dispatchBuild();
+
+        // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked,
+        // despite visual stability being active
+        verifyBuiltList(
+                notif(2),
+                notif(0),
+                notif(1)
+        );
+    }
+
+    @Test
+    public void groupRevertingToSummaryRetainsStablePosition() {
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
+
+        // GIVEN a notification group is on screen
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(2);
+        addNotif(1, PACKAGE_1).setRank(3);
+        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+        addGroupChild(3, PACKAGE_1, "group").setRank(5);
+        addGroupChild(4, PACKAGE_1, "group").setRank(6);
+        dispatchBuild();
+
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                group(
+                        summary(2),
+                        child(3),
+                        child(4)
+                )
+        );
+
+        // WHEN the notification summary rank increases and children removed
+        setNewRank(notif(2).entry, 1);
+        mEntrySet.remove(4);
+        mEntrySet.remove(3);
+        dispatchBuild();
+
+        // VERIFY the summary stays in the same location on rebuild
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2)
+        );
+    }
+
     private static void setNewRank(NotificationEntry entry, int rank) {
         entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build());
     }
@@ -2255,26 +2450,35 @@
         return addGroupChildWithTag(index, packageId, groupId, null);
     }
 
-    private void assertOrder(String visible, String active, String expected) {
+    private void assertOrder(String visible, String active, String expected,
+            boolean isOrderedCorrectly) {
         StringBuilder differenceSb = new StringBuilder();
+        NotifSection section = new NotifSection(mock(NotifSectioner.class), 0);
         for (char c : active.toCharArray()) {
             if (visible.indexOf(c) < 0) differenceSb.append(c);
         }
         String difference = differenceSb.toString();
 
+        int globalIndex = 0;
         for (int i = 0; i < visible.length(); i++) {
-            addNotif(i, String.valueOf(visible.charAt(i)))
-                    .setRank(active.indexOf(visible.charAt(i)))
+            final char c = visible.charAt(i);
+            // Skip notifications which aren't active anymore
+            if (!active.contains(String.valueOf(c))) continue;
+            addNotif(globalIndex++, String.valueOf(c))
+                    .setRank(active.indexOf(c))
+                    .setSection(section)
                     .setStableIndex(i);
-
         }
 
-        for (int i = 0; i < difference.length(); i++) {
-            addNotif(i + visible.length(), String.valueOf(difference.charAt(i)))
-                    .setRank(active.indexOf(difference.charAt(i)))
+        for (char c : difference.toCharArray()) {
+            addNotif(globalIndex++, String.valueOf(c))
+                    .setRank(active.indexOf(c))
+                    .setSection(section)
                     .setStableIndex(-1);
         }
 
+        clearInvocations(mStabilityManager);
+
         dispatchBuild();
         StringBuilder resultSb = new StringBuilder();
         for (int i = 0; i < expected.length(); i++) {
@@ -2284,6 +2488,9 @@
         assertEquals("visible [" + visible + "] active [" + active + "]",
                 expected, resultSb.toString());
         mEntrySet.clear();
+
+        verify(mStabilityManager, isOrderedCorrectly ? never() : times(1))
+                .onEntryReorderSuppressed();
     }
 
     private int nextId(String packageName) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 340bc96..3ff7639 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -674,7 +674,9 @@
     @Test
     fun testOnRankingApplied_newEntryShouldAlert() {
         // GIVEN that mEntry has never interrupted in the past, and now should
+        // and is new enough to do so
         assertFalse(mEntry.hasInterrupted())
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis())
         setShouldHeadsUp(mEntry)
         whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
 
@@ -690,8 +692,9 @@
 
     @Test
     fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() {
-        // GIVEN that mEntry has alerted in the past
+        // GIVEN that mEntry has alerted in the past, even if it's new
         mEntry.setInterruption()
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis())
         setShouldHeadsUp(mEntry)
         whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
 
@@ -725,6 +728,27 @@
         verify(mHeadsUpManager).showNotification(mEntry)
     }
 
+    @Test
+    fun testOnRankingApplied_entryUpdatedButTooOld() {
+        // GIVEN that mEntry is added in a state where it should not HUN
+        setShouldHeadsUp(mEntry, false)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // and it was actually added 10s ago
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis() - 10000)
+
+        // WHEN it is updated to HUN and then a ranking update occurs
+        setShouldHeadsUp(mEntry)
+        whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+        mCollectionListener.onRankingApplied()
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN the notification is never bound or shown
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+        verify(mHeadsUpManager, never()).showNotification(any())
+    }
+
     private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
         whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
         whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index dcf2455..b6b0b77 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -261,23 +261,15 @@
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
 
         // WHEN notification is moved under a parent
-        NotificationEntry groupSummary = getNotificationEntryBuilder()
-                .setParent(ROOT_ENTRY)
-                .setGroupSummary(mContext, true)
-                .setGroup(mContext, TEST_GROUP_KEY)
-                .build();
-        GroupEntry parent = mock(GroupEntry.class);
-        when(parent.getSummary()).thenReturn(groupSummary);
-        NotificationEntryBuilder.setNewParent(mEntry, parent);
-        mCollectionListener.onEntryInit(groupSummary);
-        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry, groupSummary));
+        NotificationEntryBuilder.setNewParent(mEntry, mock(GroupEntry.class));
+        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
 
         // THEN we rebind it as not-minimized
         verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
         assertFalse(mParamsCaptor.getValue().isLowPriority());
 
-        // THEN we filter it because the parent summary is not yet inflated.
-        assertTrue(mUninflatedFilter.shouldFilterOut(mEntry, 0));
+        // THEN we do not filter it because it's not the first inflation.
+        assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
     }
 
     @Test
@@ -401,6 +393,36 @@
     }
 
     @Test
+    public void testNullGroupSummary() {
+        // GIVEN a newly-posted group with a summary and two children
+        final GroupEntry group = new GroupEntryBuilder()
+                .setCreationTime(400)
+                .setSummary(getNotificationEntryBuilder().setId(1).build())
+                .addChild(getNotificationEntryBuilder().setId(2).build())
+                .addChild(getNotificationEntryBuilder().setId(3).build())
+                .build();
+        fireAddEvents(List.of(group));
+        final NotificationEntry child0 = group.getChildren().get(0);
+        final NotificationEntry child1 = group.getChildren().get(1);
+        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
+
+        // WHEN the summary is pruned
+        new GroupEntryBuilder()
+                .setCreationTime(400)
+                .addChild(child0)
+                .addChild(child1)
+                .build();
+
+        // WHEN all of the children (but not the summary) finish inflating
+        mNotifInflater.invokeInflateCallbackForEntry(child0);
+        mNotifInflater.invokeInflateCallbackForEntry(child1);
+
+        // THEN the entire group is not filtered out
+        assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401));
+        assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401));
+    }
+
+    @Test
     public void testPartiallyInflatedGroupsAreNotFilteredOutIfSummaryReinflate() {
         // GIVEN a newly-posted group with a summary and two children
         final String groupKey = "test_reinflate_group";
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
new file mode 100644
index 0000000..1cdd023
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.Log
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class SemiStableSortTest : SysuiTestCase() {
+
+    var shuffleInput: Boolean = false
+    var testStabilizeTo: Boolean = false
+    var sorter: SemiStableSort? = null
+
+    @Before
+    fun setUp() {
+        shuffleInput = false
+        sorter = null
+    }
+
+    private fun stringStabilizeTo(
+        stableOrder: String,
+        activeOrder: String,
+    ): Pair<String, Boolean> {
+        val actives = activeOrder.toMutableList()
+        val result = mutableListOf<Char>()
+        return (sorter ?: SemiStableSort())
+            .stabilizeTo(
+                actives,
+                { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+                result,
+            )
+            .let { ordered -> result.joinToString("") to ordered }
+    }
+
+    private fun stringSort(
+        stableOrder: String,
+        activeOrder: String,
+    ): Pair<String, Boolean> {
+        val actives = activeOrder.toMutableList()
+        if (shuffleInput) {
+            actives.shuffle()
+        }
+        return (sorter ?: SemiStableSort())
+            .sort(
+                actives,
+                { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+                compareBy { activeOrder.indexOf(it) },
+            )
+            .let { ordered -> actives.joinToString("") to ordered }
+    }
+
+    private fun testCase(
+        stableOrder: String,
+        activeOrder: String,
+        expected: String,
+        expectOrdered: Boolean,
+    ) {
+        val (mergeResult, ordered) =
+            if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder)
+            else stringSort(stableOrder, activeOrder)
+        val resultPass = expected == mergeResult
+        val orderedPass = ordered == expectOrdered
+        val pass = resultPass && orderedPass
+        val resultSuffix =
+            if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult"
+        val orderedSuffix =
+            if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered"
+        val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix"
+        Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult")
+        if (!pass) {
+            throw AssertionError("Test case failed: $readableResult")
+        }
+    }
+
+    private fun runAllTestCases() {
+        // No input or output
+        testCase("", "", "", true)
+        // Remove everything
+        testCase("ABCDEFG", "", "", true)
+        // Literally no changes
+        testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true)
+
+        // No stable order
+        testCase("", "ABCDEFG", "ABCDEFG", true)
+
+        // F moved after A, and...
+        testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F
+        testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F
+        testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was
+
+        // B moved after F, and...
+        testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B
+        testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B
+        testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was
+
+        // Swap F and B, and...
+        testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F
+        testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F
+        testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG)
+        testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG)
+        testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B
+        testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B
+
+        // Remove a bunch of entries at once
+        testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true)
+
+        // Remove a bunch of entries and scramble
+        testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false)
+
+        // Add a bunch of entries at once
+        testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true)
+
+        // Add a bunch of entries and reverse originals
+        // NOTE: Some of these don't have obviously correct answers
+        testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended
+        testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended
+        testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append
+        testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend
+        testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries
+
+        // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+        testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false)
+    }
+
+    @Test
+    fun testSort() {
+        testStabilizeTo = false
+        shuffleInput = false
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testSortWithSingleInstance() {
+        testStabilizeTo = false
+        shuffleInput = false
+        sorter = SemiStableSort()
+        runAllTestCases()
+    }
+
+    @Test
+    fun testSortWithShuffledInput() {
+        testStabilizeTo = false
+        shuffleInput = true
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testStabilizeTo() {
+        testStabilizeTo = true
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testStabilizeToWithSingleInstance() {
+        testStabilizeTo = true
+        sorter = SemiStableSort()
+        runAllTestCases()
+    }
+
+    @Test
+    fun testIsSorted() {
+        val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) }
+        SemiStableSort.apply {
+            assertTrue(emptyList<Int>().isSorted(intCmp))
+            assertTrue(listOf(1).isSorted(intCmp))
+            assertTrue(listOf(1, 2).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp))
+            assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp))
+            assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp))
+            assertFalse(listOf(2, 1).isSorted(intCmp))
+            assertFalse(listOf(2, 1, 2).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 1).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp))
+            assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp))
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
new file mode 100644
index 0000000..2036954
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class ShadeListBuilderHelperTest : SysuiTestCase() {
+
+    @Test
+    fun testGetContiguousSubLists() {
+        assertThat(getContiguousSubLists("AAAAAA".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A', 'A', 'A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBB".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABAA".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B'),
+                listOf('A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B', 'B'),
+                listOf('C', 'C'),
+                listOf('D'),
+                listOf('E', 'E', 'E'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B', 'B'),
+                listOf('C', 'C'),
+                listOf('E', 'E', 'E'),
+            )
+            .inOrder()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 46f630b..ea311da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -51,12 +51,14 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -97,6 +99,7 @@
     NotifPipelineFlags mFlags;
     @Mock
     KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+    UiEventLoggerFake mUiEventLoggerFake;
     @Mock
     PendingIntent mPendingIntent;
 
@@ -107,6 +110,8 @@
         MockitoAnnotations.initMocks(this);
         when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false);
 
+        mUiEventLoggerFake = new UiEventLoggerFake();
+
         mNotifInterruptionStateProvider =
                 new NotificationInterruptStateProviderImpl(
                         mContext.getContentResolver(),
@@ -120,7 +125,8 @@
                         mLogger,
                         mMockHandler,
                         mFlags,
-                        mKeyguardNotificationVisibilityProvider);
+                        mKeyguardNotificationVisibilityProvider,
+                        mUiEventLoggerFake);
         mNotifInterruptionStateProvider.mUseHeadsUp = true;
     }
 
@@ -442,6 +448,13 @@
         verify(mLogger, never()).logNoFullscreen(any(), any());
         verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
         verify(mLogger, never()).logFullscreen(any(), any());
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
+        UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
+        assertThat(fakeUiEvent.eventId).isEqualTo(
+                NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR.getId());
+        assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
+        assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
     }
 
     @Test
@@ -600,6 +613,13 @@
         verify(mLogger, never()).logNoFullscreen(any(), any());
         verify(mLogger).logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
         verify(mLogger, never()).logFullscreen(any(), any());
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
+        UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
+        assertThat(fakeUiEvent.eventId).isEqualTo(
+                NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD.getId());
+        assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
+        assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 11798a7..87f4c32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -361,6 +361,22 @@
         assertThat(sut.isOnKeyguard).isFalse()
     }
     // endregion
+
+    // region mIsClosing
+    @Test
+    fun isClosing_whenShadeClosing_shouldReturnTrue() {
+        sut.setIsClosing(true)
+
+        assertThat(sut.isClosing).isTrue()
+    }
+
+    @Test
+    fun isClosing_whenShadeFinishClosing_shouldReturnFalse() {
+        sut.setIsClosing(false)
+
+        assertThat(sut.isClosing).isFalse()
+    }
+    // endregion
 }
 
 // region Arrange helper methods.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 4353036..35c8b61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -728,6 +728,57 @@
         verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat());
     }
 
+    @Test
+    public void testSetOwnScrollY_shadeNotClosing_scrollYChanges() {
+        // Given: shade is not closing, scrollY is 0
+        mAmbientState.setScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+        mAmbientState.setIsClosing(false);
+
+        // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+        mStackScroller.setOwnScrollY(1);
+
+        // Then: scrollY should be set to 1
+        assertEquals(1, mAmbientState.getScrollY());
+
+        // Reset scrollY back to 0 to avoid interfering with other tests
+        mStackScroller.setOwnScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+    }
+
+    @Test
+    public void testSetOwnScrollY_shadeClosing_scrollYDoesNotChange() {
+        // Given: shade is closing, scrollY is 0
+        mAmbientState.setScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+        mAmbientState.setIsClosing(true);
+
+        // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+        mStackScroller.setOwnScrollY(1);
+
+        // Then: scrollY should not change, it should still be 0
+        assertEquals(0, mAmbientState.getScrollY());
+
+        // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests
+        mAmbientState.setIsClosing(false);
+        mStackScroller.setOwnScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+    }
+
+    @Test
+    public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() {
+        // Given: mAmbientState.mIsClosing is set to be true
+        // mIsExpanded is set to be false
+        mAmbientState.setIsClosing(true);
+        mStackScroller.setIsExpanded(false);
+
+        // When: onExpansionStopped is called
+        mStackScroller.onExpansionStopped();
+
+        // Then: mAmbientState.scrollY should be set to be 0
+        assertEquals(mAmbientState.getScrollY(), 0);
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index ad497a2..6de8bd5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -80,6 +80,7 @@
 
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.jank.InteractionJankMonitor;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.logging.testing.FakeMetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
@@ -98,7 +99,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -271,7 +273,6 @@
     @Mock private OngoingCallController mOngoingCallController;
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock private LockscreenShadeTransitionController mLockscreenTransitionController;
-    @Mock private FeatureFlags mFeatureFlags;
     @Mock private NotificationVisibilityProvider mVisibilityProvider;
     @Mock private WallpaperManager mWallpaperManager;
     @Mock private IWallpaperManager mIWallpaperManager;
@@ -296,9 +297,10 @@
 
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
-    private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
-    private FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
-    private InitController mInitController = new InitController();
+    private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
+    private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    private final InitController mInitController = new InitController();
     private final DumpManager mDumpManager = new DumpManager();
 
     @Before
@@ -322,7 +324,8 @@
                         mock(NotificationInterruptLogger.class),
                         new Handler(TestableLooper.get(this).getLooper()),
                         mock(NotifPipelineFlags.class),
-                        mock(KeyguardNotificationVisibilityProvider.class));
+                        mock(KeyguardNotificationVisibilityProvider.class),
+                        mock(UiEventLogger.class));
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
         mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));
@@ -1017,6 +1020,60 @@
     }
 
     @Test
+    public void collapseShade_callsAnimateCollapsePanels_whenExpanded() {
+        // GIVEN the shade is expanded
+        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+
+        // WHEN collapseShade is called
+        mCentralSurfaces.collapseShade();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() {
+        // GIVEN the shade is collapsed
+        mCentralSurfaces.setPanelExpanded(false);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+
+        // WHEN collapseShade is called
+        mCentralSurfaces.collapseShade();
+
+        // VERIFY that animateCollapsePanels is NOT called
+        verify(mShadeController, never()).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() {
+        // GIVEN the shade is expanded & flag enabled
+        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+        mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, false);
+
+        // WHEN collapseShadeForBugreport is called
+        mCentralSurfaces.collapseShadeForBugreport();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() {
+        // GIVEN the shade is expanded & flag enabled
+        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+        mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, true);
+
+        // WHEN collapseShadeForBugreport is called
+        mCentralSurfaces.collapseShadeForBugreport();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController, never()).animateCollapsePanels();
+    }
+
+    @Test
     public void deviceStateChange_unfolded_shadeOpen_setsLeaveOpenOnKeyguardHide() {
         when(mKeyguardStateController.isShowing()).thenReturn(false);
         setFoldedStates(FOLD_STATE_FOLDED);
@@ -1102,7 +1159,8 @@
                 NotificationInterruptLogger logger,
                 Handler mainHandler,
                 NotifPipelineFlags flags,
-                KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+                KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+                UiEventLogger uiEventLogger) {
             super(
                     contentResolver,
                     powerManager,
@@ -1115,7 +1173,8 @@
                     logger,
                     mainHandler,
                     flags,
-                    keyguardNotificationVisibilityProvider
+                    keyguardNotificationVisibilityProvider,
+                    uiEventLogger
             );
             mUseHeadsUp = true;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 8da8d04..0c35659 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -117,7 +117,6 @@
     @Mock private BouncerCallbackInteractor mBouncerCallbackInteractor;
     @Mock private BouncerInteractor mBouncerInteractor;
     @Mock private BouncerView mBouncerView;
-//    @Mock private WeakReference<BouncerViewDelegate> mBouncerViewDelegateWeakReference;
     @Mock private BouncerViewDelegate mBouncerViewDelegate;
 
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
index 65e2964..3a0a94d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 3a006ad..36e76f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -49,9 +49,9 @@
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogcatEchoTracker;
 import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogcatEchoTracker;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
index bf43238..eba3b04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
@@ -20,7 +20,6 @@
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
@@ -34,8 +33,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -91,7 +90,7 @@
     fun testStartActivity() {
         `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false)
         statusBarUserSwitcherContainer.callOnClick()
-        verify(userSwitcherDialogController).showDialog(any(View::class.java))
+        verify(userSwitcherDialogController).showDialog(any(), any())
         `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true)
         statusBarUserSwitcherContainer.callOnClick()
         verify(activityStarter).startActivity(any(Intent::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
index 0e75c74..b32058f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
@@ -22,7 +22,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index f751afc..2f18ce3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -27,6 +27,9 @@
     private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
     override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled
 
+    private val _isWifiDefault: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault
+
     private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> =
         MutableStateFlow(WifiNetworkModel.Inactive)
     override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
@@ -38,6 +41,10 @@
         _isWifiEnabled.value = enabled
     }
 
+    fun setIsWifiDefault(default: Boolean) {
+        _isWifiDefault.value = default
+    }
+
     fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) {
         _wifiNetwork.value = wifiNetworkModel
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index 0ba0bd6..a64a4bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -222,6 +222,83 @@
     }
 
     @Test
+    fun isWifiDefault_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_wifiNetwork_isTrue() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val wifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(SSID)
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(
+            NETWORK,
+            createWifiNetworkCapabilities(wifiInfo)
+        )
+
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_cellularVcnNetwork_isTrue() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_cellularNotVcnNetwork_isFalse() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(mock())
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_wifiNetworkLost_isFalse() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        // First, add a network
+        getDefaultNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        // WHEN the network is lost
+        getDefaultNetworkCallback().onLost(NETWORK)
+
+        // THEN we update to false
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
     fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
         var latest: WifiNetworkModel? = null
         val job = underTest
@@ -745,6 +822,12 @@
         return callbackCaptor.value!!
     }
 
+    private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
+        val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
+        verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
     private fun createWifiNetworkCapabilities(
         wifiInfo: WifiInfo,
         isValidated: Boolean = true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
index 39b886a..71b8bab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -178,6 +178,29 @@
     }
 
     @Test
+    fun isDefault_matchesRepoIsDefault() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .isDefault
+            .onEach { latest = it }
+            .launchIn(this)
+
+        wifiRepository.setIsWifiDefault(true)
+        yield()
+        assertThat(latest).isTrue()
+
+        wifiRepository.setIsWifiDefault(false)
+        yield()
+        assertThat(latest).isFalse()
+
+        wifiRepository.setIsWifiDefault(true)
+        yield()
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
     fun wifiNetwork_matchesRepoWifiNetwork() = runBlocking(IMMEDIATE) {
         val wifiNetwork = WifiNetworkModel.Active(
             networkId = 45,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index a3ad028..7686071 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -22,7 +22,7 @@
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
@@ -88,6 +88,7 @@
     fun wifiIcon() =
         runBlocking(IMMEDIATE) {
             wifiRepository.setIsWifiEnabled(testCase.enabled)
+            wifiRepository.setIsWifiDefault(testCase.isDefault)
             connectivityRepository.setForceHiddenIcons(
                 if (testCase.forceHidden) {
                     setOf(ConnectivitySlot.WIFI)
@@ -125,19 +126,12 @@
                 } else {
                     testCase.expected.contentDescription.invoke(context)
                 }
-            assertThat(iconFlow.value?.contentDescription?.getAsString())
+            assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context))
                 .isEqualTo(expectedContentDescription)
 
             job.cancel()
         }
 
-    private fun ContentDescription.getAsString(): String? {
-        return when (this) {
-            is ContentDescription.Loaded -> this.description
-            is ContentDescription.Resource -> context.getString(this.res)
-        }
-    }
-
     internal data class Expected(
         /** The resource that should be used for the icon. */
         @DrawableRes val iconResource: Int,
@@ -159,6 +153,7 @@
         val forceHidden: Boolean = false,
         val alwaysShowIconWhenEnabled: Boolean = false,
         val hasDataCapabilities: Boolean = true,
+        val isDefault: Boolean = false,
         val network: WifiNetworkModel,
 
         /** The expected output. Null if we expect the output to be null. */
@@ -169,6 +164,7 @@
                 "forceHidden=$forceHidden, " +
                 "showWhenEnabled=$alwaysShowIconWhenEnabled, " +
                 "hasDataCaps=$hasDataCapabilities, " +
+                "isDefault=$isDefault, " +
                 "network=$network) then " +
                 "EXPECTED($expected)"
         }
@@ -303,6 +299,46 @@
                         ),
                 ),
 
+                // isDefault = true => all Inactive and Active networks shown
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Inactive,
+                    expected =
+                        Expected(
+                            iconResource = WIFI_NO_NETWORK,
+                            contentDescription = { context ->
+                                "${context.getString(WIFI_NO_CONNECTION)}," +
+                                    context.getString(NO_INTERNET)
+                            },
+                            description = "No network icon",
+                        ),
+                ),
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3),
+                    expected =
+                        Expected(
+                            iconResource = WIFI_NO_INTERNET_ICONS[3],
+                            contentDescription = { context ->
+                                "${context.getString(WIFI_CONNECTION_STRENGTH[3])}," +
+                                    context.getString(NO_INTERNET)
+                            },
+                            description = "No internet level 3 icon",
+                        ),
+                ),
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1),
+                    expected =
+                        Expected(
+                            iconResource = WIFI_FULL_ICONS[1],
+                            contentDescription = { context ->
+                                context.getString(WIFI_CONNECTION_STRENGTH[1])
+                            },
+                            description = "Full internet level 1 icon",
+                        ),
+                ),
+
                 // network = CarrierMerged => not shown
                 TestCase(
                     network = WifiNetworkModel.CarrierMerged,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 3169eef..79633d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -76,6 +76,8 @@
         scope.cancel()
     }
 
+    // See [WifiViewModelIconParameterizedTest] for additional view model tests.
+
     // Note on testing: [WifiViewModel] exposes 3 different instances of
     // [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact
     // same data for icon, activity, etc. flows. So, most of these tests will test just one of the
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
index c9f2b4d..13e9f60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
@@ -19,9 +19,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 6225d0c..9fbf159 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -16,11 +16,8 @@
 
 package com.android.systemui.temporarydisplay.chipbar
 
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
-import android.media.MediaRoute2Info
 import android.os.PowerManager
+import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
@@ -31,19 +28,19 @@
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
-import com.android.systemui.media.taptotransfer.sender.ChipStateSender
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEvents
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.google.common.truth.Truth.assertThat
@@ -53,7 +50,6 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
@@ -64,437 +60,293 @@
 class ChipbarCoordinatorTest : SysuiTestCase() {
     private lateinit var underTest: FakeChipbarCoordinator
 
-    @Mock
-    private lateinit var packageManager: PackageManager
-    @Mock
-    private lateinit var applicationInfo: ApplicationInfo
-    @Mock
-    private lateinit var logger: MediaTttLogger
-    @Mock
-    private lateinit var accessibilityManager: AccessibilityManager
-    @Mock
-    private lateinit var configurationController: ConfigurationController
-    @Mock
-    private lateinit var powerManager: PowerManager
-    @Mock
-    private lateinit var windowManager: WindowManager
-    @Mock
-    private lateinit var falsingManager: FalsingManager
-    @Mock
-    private lateinit var falsingCollector: FalsingCollector
-    @Mock
-    private lateinit var viewUtil: ViewUtil
-    private lateinit var fakeAppIconDrawable: Drawable
+    @Mock private lateinit var logger: MediaTttLogger
+    @Mock private lateinit var accessibilityManager: AccessibilityManager
+    @Mock private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var powerManager: PowerManager
+    @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var viewUtil: ViewUtil
+    @Mock private lateinit var vibratorHelper: VibratorHelper
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
-    private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-
-        fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
-        whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
-        whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
-        whenever(packageManager.getApplicationInfo(
-            eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>()
-        )).thenReturn(applicationInfo)
-        context.setMockPackageManager(packageManager)
+        whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
 
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
 
         uiEventLoggerFake = UiEventLoggerFake()
-        senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
 
-        whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
-
-        underTest = FakeChipbarCoordinator(
-            context,
-            logger,
-            windowManager,
-            fakeExecutor,
-            accessibilityManager,
-            configurationController,
-            powerManager,
-            senderUiEventLogger,
-            falsingManager,
-            falsingCollector,
-            viewUtil,
-        )
+        underTest =
+            FakeChipbarCoordinator(
+                context,
+                logger,
+                windowManager,
+                fakeExecutor,
+                accessibilityManager,
+                configurationController,
+                powerManager,
+                falsingManager,
+                falsingCollector,
+                viewUtil,
+                vibratorHelper,
+            )
         underTest.start()
     }
 
     @Test
-    fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
-        val state = almostCloseToStartCast()
-        underTest.displayView(state)
+    fun displayView_loadedIcon_correctlyRendered() {
+        val drawable = context.getDrawable(R.drawable.ic_celebration)!!
 
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
-        val state = almostCloseToEndCast()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() {
-        val state = transferToReceiverTriggered()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() {
-        val state = transferToThisDeviceTriggered()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() {
-        val state = transferToReceiverSucceeded()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_nullUndoRunnable_noUndo() {
-        underTest.displayView(transferToReceiverSucceeded(undoCallback = null))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_undoWithClick() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() {
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() {
-        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isFalse()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() {
-        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-
-        getChipView().getUndoButton().performClick()
-
-        assertThat(getChipView().getChipText()).isEqualTo(
-            transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
-            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id
-        )
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() {
-        val state = transferToThisDeviceSucceeded()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() {
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback = null))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() {
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-
-        getChipView().getUndoButton().performClick()
-
-        assertThat(getChipView().getChipText()).isEqualTo(
-            transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
-            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id
-        )
-    }
-
-    @Test
-    fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() {
-        val state = transferToReceiverFailed()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(getChipView().getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() {
-        val state = transferToThisDeviceFailed()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(getChipView().getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun changeFromAlmostCloseToStartToTransferTriggered_loadingIconAppears() {
-        underTest.displayView(almostCloseToStartCast())
-        underTest.displayView(transferToReceiverTriggered())
-
-        assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() {
-        underTest.displayView(transferToReceiverTriggered())
-        underTest.displayView(transferToReceiverSucceeded())
-
-        assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() {
-        underTest.displayView(transferToReceiverTriggered())
         underTest.displayView(
-            transferToReceiverSucceeded(
-                object : IUndoMediaTransferCallback.Stub() {
-                    override fun onUndoTriggered() {}
-                }
+            ChipbarInfo(
+                Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+                Text.Loaded("text"),
+                endItem = null,
             )
         )
 
-        assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        val iconView = getChipbarView().getStartIconView()
+        assertThat(iconView.drawable).isEqualTo(drawable)
+        assertThat(iconView.contentDescription).isEqualTo("loadedCD")
     }
 
     @Test
-    fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() {
-        underTest.displayView(transferToReceiverSucceeded())
-        underTest.displayView(almostCloseToStartCast())
+    fun displayView_resourceIcon_correctlyRendered() {
+        val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout)
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription),
+                Text.Loaded("text"),
+                endItem = null,
+            )
+        )
 
-        assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE)
+        val iconView = getChipbarView().getStartIconView()
+        assertThat(iconView.contentDescription)
+            .isEqualTo(contentDescription.loadContentDescription(context))
     }
 
     @Test
-    fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() {
-        underTest.displayView(transferToReceiverTriggered())
-        underTest.displayView(transferToReceiverFailed())
+    fun displayView_loadedText_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("display view text here"),
+                endItem = null,
+            )
+        )
 
-        assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(getChipbarView().getChipText()).isEqualTo("display view text here")
     }
 
-    private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
+    @Test
+    fun displayView_resourceText_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Resource(R.string.screenrecord_start_error),
+                endItem = null,
+            )
+        )
+
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(context.getString(R.string.screenrecord_start_error))
+    }
+
+    @Test
+    fun displayView_endItemNull_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = null,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemLoading_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemError_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Error,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemButton_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        onClickListener = {},
+                    ),
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getEndButton().text).isEqualTo("button text")
+        assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun displayView_endItemButtonClicked_falseTap_listenerNotRun() {
+        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
+        var isClicked = false
+        val buttonClickListener = View.OnClickListener { isClicked = true }
+
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        buttonClickListener,
+                    ),
+            )
+        )
+
+        getChipbarView().getEndButton().performClick()
+
+        assertThat(isClicked).isFalse()
+    }
+
+    @Test
+    fun displayView_endItemButtonClicked_notFalseTap_listenerRun() {
+        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
+        var isClicked = false
+        val buttonClickListener = View.OnClickListener { isClicked = true }
+
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        buttonClickListener,
+                    ),
+            )
+        )
+
+        getChipbarView().getEndButton().performClick()
+
+        assertThat(isClicked).isTrue()
+    }
+
+    @Test
+    fun displayView_vibrationEffect_doubleClickEffect() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = null,
+                vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK),
+            )
+        )
+
+        verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))
+    }
+
+    @Test
+    fun updateView_viewUpdated() {
+        // First, display a view
+        val drawable = context.getDrawable(R.drawable.ic_celebration)!!
+
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable)
+        assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD")
+        assertThat(chipbarView.getChipText()).isEqualTo("title text")
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+
+        // WHEN the view is updated
+        val newDrawable = context.getDrawable(R.drawable.ic_cake)!!
+        underTest.updateView(
+            ChipbarInfo(
+                Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")),
+                Text.Loaded("new title text"),
+                endItem = ChipbarEndItem.Error,
+            ),
+            chipbarView
+        )
+
+        // THEN we display the new view
+        assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable)
+        assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD")
+        assertThat(chipbarView.getChipText()).isEqualTo("new title text")
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon)
 
     private fun ViewGroup.getChipText(): String =
         (this.requireViewById<TextView>(R.id.text)).text as String
 
-    private fun ViewGroup.getLoadingIconVisibility(): Int =
-        this.requireViewById<View>(R.id.loading).visibility
+    private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
 
-    private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo)
+    private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button)
 
-    private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon)
+    private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
 
-    private fun getChipView(): ViewGroup {
+    private fun getChipbarView(): ViewGroup {
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
         verify(windowManager).addView(viewCaptor.capture(), any())
         return viewCaptor.value as ViewGroup
     }
-
-    // TODO(b/245610654): For now, the below methods are duplicated between this test and
-    //   [MediaTttSenderCoordinatorTest]. Once we define a generic API for [ChipbarCoordinator],
-    //   these will no longer be duplicated.
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToStartCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToEndCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
 }
 
-private const val APP_NAME = "Fake app name"
-private const val OTHER_DEVICE_NAME = "My Tablet"
-private const val PACKAGE_NAME = "com.android.systemui"
 private const val TIMEOUT = 10000
-
-private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
-    .addFeature("feature")
-    .setClientPackageName(PACKAGE_NAME)
-    .build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
index 10704ac..17d4023 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -24,8 +24,8 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
@@ -39,10 +39,10 @@
     accessibilityManager: AccessibilityManager,
     configurationController: ConfigurationController,
     powerManager: PowerManager,
-    uiEventLogger: MediaTttSenderUiEventLogger,
     falsingManager: FalsingManager,
     falsingCollector: FalsingCollector,
     viewUtil: ViewUtil,
+    vibratorHelper: VibratorHelper,
 ) :
     ChipbarCoordinator(
         context,
@@ -52,10 +52,10 @@
         accessibilityManager,
         configurationController,
         powerManager,
-        uiEventLogger,
         falsingManager,
         falsingCollector,
         viewUtil,
+        vibratorHelper,
     ) {
     override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
index d951f36..525d837 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
@@ -110,7 +110,7 @@
         val thirdExpectedValue =
             setUpUsers(
                 count = 2,
-                hasGuest = true,
+                isLastGuestUser = true,
                 selectedIndex = 1,
             )
         underTest.refreshUsers()
@@ -121,21 +121,25 @@
     }
 
     @Test
-    fun `refreshUsers - sorts by creation time`() = runSelfCancelingTest {
+    fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest {
         underTest = create(this)
         val unsortedUsers =
             setUpUsers(
                 count = 3,
                 selectedIndex = 0,
+                isLastGuestUser = true,
             )
-        unsortedUsers[0].creationTime = 900
-        unsortedUsers[1].creationTime = 700
-        unsortedUsers[2].creationTime = 999
-        val expectedUsers = listOf(unsortedUsers[1], unsortedUsers[0], unsortedUsers[2])
+        unsortedUsers[0].creationTime = 999
+        unsortedUsers[1].creationTime = 900
+        unsortedUsers[2].creationTime = 950
+        val expectedUsers =
+            listOf(
+                unsortedUsers[1],
+                unsortedUsers[0],
+                unsortedUsers[2], // last because this is the guest
+            )
         var userInfos: List<UserInfo>? = null
-        var selectedUserInfo: UserInfo? = null
         underTest.userInfos.onEach { userInfos = it }.launchIn(this)
-        underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
 
         underTest.refreshUsers()
         assertThat(userInfos).isEqualTo(expectedUsers)
@@ -143,14 +147,14 @@
 
     private fun setUpUsers(
         count: Int,
-        hasGuest: Boolean = false,
+        isLastGuestUser: Boolean = false,
         selectedIndex: Int = 0,
     ): List<UserInfo> {
         val userInfos =
             (0 until count).map { index ->
                 createUserInfo(
                     index,
-                    isGuest = hasGuest && index == count - 1,
+                    isGuest = isLastGuestUser && index == count - 1,
                 )
             }
         whenever(manager.aliveUsers).thenReturn(userInfos)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
index d4b41c1..a363a03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
@@ -97,6 +97,7 @@
                         createUserRecord(2),
                         createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
                         createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+                        createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
                     )
                 )
             var models: List<UserModel>? = null
@@ -176,15 +177,17 @@
                         createUserRecord(2),
                         createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
                         createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+                        createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
                     )
                 )
             var models: List<UserActionModel>? = null
             val job = underTest.actions.onEach { models = it }.launchIn(this)
 
-            assertThat(models).hasSize(3)
+            assertThat(models).hasSize(4)
             assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
             assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
             assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
+            assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
             job.cancel()
         }
 
@@ -200,6 +203,7 @@
             isAddUser = action == UserActionModel.ADD_USER,
             isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
             isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+            isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
index e80d516..f682e31 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
@@ -28,6 +28,7 @@
 import com.android.internal.R.drawable.ic_account_circle
 import com.android.systemui.R
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.source.UserRecord
 import com.android.systemui.user.domain.model.ShowDialogRequestModel
@@ -317,14 +318,16 @@
             keyguardRepository.setKeyguardShowing(false)
             var dialogRequest: ShowDialogRequestModel? = null
             val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogShower: UserSwitchDialogController.DialogShower = mock()
 
-            underTest.executeAction(UserActionModel.ADD_USER)
+            underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
             assertThat(dialogRequest)
                 .isEqualTo(
                     ShowDialogRequestModel.ShowAddUserDialog(
                         userHandle = userInfos[0].userHandle,
                         isKeyguardShowing = false,
                         showEphemeralMessage = false,
+                        dialogShower = dialogShower,
                     )
                 )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
index c3a9705..6a17c8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
@@ -64,13 +64,7 @@
     @Test
     fun `actions - not actionable when locked and not locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(false)
             keyguardRepository.setKeyguardShowing(false)
 
@@ -92,13 +86,7 @@
     @Test
     fun `actions - actionable when locked and not locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(true)
             keyguardRepository.setKeyguardShowing(false)
 
@@ -120,13 +108,7 @@
     @Test
     fun `actions - actionable when locked and locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(true)
             keyguardRepository.setKeyguardShowing(true)
 
@@ -182,6 +164,10 @@
         verify(activityStarter).startActivity(any(), anyBoolean())
     }
 
+    private fun setActions() {
+        userRepository.setActions(UserActionModel.values().toList())
+    }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 0344e3f..c12a868 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -268,6 +268,26 @@
         }
 
     @Test
+    fun `menu actions`() =
+        runBlocking(IMMEDIATE) {
+            userRepository.setActions(UserActionModel.values().toList())
+            var actions: List<UserActionViewModel>? = null
+            val job = underTest.menu.onEach { actions = it }.launchIn(this)
+
+            assertThat(actions?.map { it.viewKey })
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
+                        UserActionModel.ADD_USER.ordinal.toLong(),
+                        UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
     fun `isFinishRequested - finishes when user is switched`() =
         runBlocking(IMMEDIATE) {
             setUsers(count = 2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
deleted file mode 100644
index 5e09b81..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
+++ /dev/null
@@ -1,131 +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.util.collection
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertThrows
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class RingBufferTest : SysuiTestCase() {
-
-    private val buffer = RingBuffer(5) { TestElement() }
-
-    private val history = mutableListOf<TestElement>()
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-    }
-
-    @Test
-    fun testBarelyFillBuffer() {
-        fillBuffer(5)
-
-        assertEquals(0, buffer[0].id)
-        assertEquals(1, buffer[1].id)
-        assertEquals(2, buffer[2].id)
-        assertEquals(3, buffer[3].id)
-        assertEquals(4, buffer[4].id)
-    }
-
-    @Test
-    fun testPartiallyFillBuffer() {
-        fillBuffer(3)
-
-        assertEquals(3, buffer.size)
-
-        assertEquals(0, buffer[0].id)
-        assertEquals(1, buffer[1].id)
-        assertEquals(2, buffer[2].id)
-
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] }
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] }
-    }
-
-    @Test
-    fun testSpinBuffer() {
-        fillBuffer(277)
-
-        assertEquals(272, buffer[0].id)
-        assertEquals(273, buffer[1].id)
-        assertEquals(274, buffer[2].id)
-        assertEquals(275, buffer[3].id)
-        assertEquals(276, buffer[4].id)
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] }
-
-        assertEquals(5, buffer.size)
-    }
-
-    @Test
-    fun testElementsAreRecycled() {
-        fillBuffer(23)
-
-        assertSame(history[4], buffer[1])
-        assertSame(history[9], buffer[1])
-        assertSame(history[14], buffer[1])
-        assertSame(history[19], buffer[1])
-    }
-
-    @Test
-    fun testIterator() {
-        fillBuffer(13)
-
-        val iterator = buffer.iterator()
-
-        for (i in 0 until 5) {
-            assertEquals(history[8 + i], iterator.next())
-        }
-        assertFalse(iterator.hasNext())
-        assertThrows(NoSuchElementException::class.java) { iterator.next() }
-    }
-
-    @Test
-    fun testForEach() {
-        fillBuffer(13)
-        var i = 8
-
-        buffer.forEach {
-            assertEquals(history[i], it)
-            i++
-        }
-        assertEquals(13, i)
-    }
-
-    private fun fillBuffer(count: Int) {
-        for (i in 0 until count) {
-            val elem = buffer.advance()
-            elem.id = history.size
-            history.add(elem)
-        }
-    }
-}
-
-private class TestElement(var id: Int = 0) {
-    override fun toString(): String {
-        return "{TestElement $id}"
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 09da52e..fa7ebf6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -80,6 +80,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
@@ -91,6 +92,7 @@
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.RankingBuilder;
@@ -190,6 +192,8 @@
     private NotificationShadeWindowView mNotificationShadeWindowView;
     @Mock
     private AuthController mAuthController;
+    @Mock
+    private ShadeExpansionStateManager mShadeExpansionStateManager;
 
     private SysUiState mSysUiState;
     private boolean mSysUiStateBubblesExpanded;
@@ -290,7 +294,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager, mKeyguardStateController,
-                mScreenOffAnimationController, mAuthController);
+                mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager);
         mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
         mNotificationShadeWindowController.attach();
 
@@ -343,7 +347,8 @@
                         mock(NotificationInterruptLogger.class),
                         mock(Handler.class),
                         mock(NotifPipelineFlags.class),
-                        mock(KeyguardNotificationVisibilityProvider.class)
+                        mock(KeyguardNotificationVisibilityProvider.class),
+                        mock(UiEventLogger.class)
                 );
         when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
         mBubbleController = new TestableBubbleController(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index 9635faf..e5316bc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -22,6 +22,7 @@
 import android.os.PowerManager;
 import android.service.dreams.IDreamManager;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
@@ -46,7 +47,8 @@
             NotificationInterruptLogger logger,
             Handler mainHandler,
             NotifPipelineFlags flags,
-            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+            UiEventLogger uiEventLogger) {
         super(contentResolver,
                 powerManager,
                 dreamManager,
@@ -58,7 +60,8 @@
                 logger,
                 mainHandler,
                 flags,
-                keyguardNotificationVisibilityProvider);
+                keyguardNotificationVisibilityProvider,
+                uiEventLogger);
         mUseHeadsUp = true;
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index 5d52be2..a60b773 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -26,7 +26,7 @@
     private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>()
 
     init {
-        Flags.getFlagFields().forEach { field ->
+        Flags.flagFields.forEach { field ->
             val flag: Flag<*> = field.get(null) as Flag<*>
             knownFlagNames[flag.id] = field.name
         }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 725b1f4..0c12680 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.common.shared.model.Position
+import com.android.systemui.keyguard.shared.model.StatusBarState
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -44,6 +45,9 @@
     private val _dozeAmount = MutableStateFlow(0f)
     override val dozeAmount: Flow<Float> = _dozeAmount
 
+    private val _statusBarState = MutableStateFlow(StatusBarState.SHADE)
+    override val statusBarState: Flow<StatusBarState> = _statusBarState
+
     override fun isKeyguardShowing(): Boolean {
         return _isKeyguardShowing.value
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
index 5272585..c33ce5d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.qs
 
-import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener
 import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -54,7 +54,7 @@
 
     override fun init() {}
 
-    override fun showDialog(viewLaunchedFrom: View?) {}
+    override fun showDialog(expandable: Expandable?) {}
 
     override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
         numRunningPackagesListeners.add(listener)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
index 2a9aedd..325da4e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
@@ -57,7 +57,6 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.GlobalSettings
-import com.android.systemui.util.time.FakeSystemClock
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.test.TestCoroutineDispatcher
 
@@ -68,7 +67,6 @@
 class FooterActionsTestUtils(
     private val context: Context,
     private val testableLooper: TestableLooper,
-    private val fakeClock: FakeSystemClock = FakeSystemClock(),
 ) {
     /** Enable or disable the user switcher in the settings. */
     fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) {
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index a94bfe2..12e7226 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -61,7 +61,7 @@
 
     // Notify the user that they should select an input method
     // Package: android
-    NOTE_SELECT_INPUT_METHOD = 8;
+    NOTE_SELECT_INPUT_METHOD = 8 [deprecated = true];
 
     // Notify the user about limited functionality before decryption
     // Package: android
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index a185b58..346fc6c 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -872,6 +872,33 @@
     }
 
     @Override
+    public void setAppWidgetHidden(String callingPackage, int hostId) {
+        final int userId = UserHandle.getCallingUserId();
+
+        if (DEBUG) {
+            Slog.i(TAG, "setAppWidgetHidden() " + userId);
+        }
+
+        mSecurityPolicy.enforceCallFromPackage(callingPackage);
+
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(userId, /* enforceUserUnlockingOrUnlocked */false);
+
+            HostId id = new HostId(Binder.getCallingUid(), hostId, callingPackage);
+            Host host = lookupHostLocked(id);
+
+            if (host != null) {
+                try {
+                    mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false);
+                } catch (NullPointerException e) {
+                    Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e);
+                    throw e;
+                }
+            }
+        }
+    }
+
+    @Override
     public void deleteAppWidgetId(String callingPackage, int appWidgetId) {
         final int userId = UserHandle.getCallingUserId();
 
diff --git a/services/companion/TEST_MAPPING b/services/companion/TEST_MAPPING
index 38d9372..37c47ba 100644
--- a/services/companion/TEST_MAPPING
+++ b/services/companion/TEST_MAPPING
@@ -8,14 +8,6 @@
     },
     {
       "name": "CtsCompanionDeviceManagerNoCompanionServicesTestCases"
-    },
-    {
-      "name": "CtsOsTestCases",
-      "options": [
-        {
-          "include-filter": "android.os.cts.CompanionDeviceManagerTest"
-        }
-      ]
     }
   ]
 }
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index ec30369b..02053cc 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -31,6 +31,7 @@
 import android.hardware.input.VirtualTouchEvent;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.IInputConstants;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Slog;
@@ -75,7 +76,7 @@
     @interface PhysType {
     }
 
-    private final Object mLock;
+    final Object mLock;
 
     /* Token -> file descriptor associations. */
     @VisibleForTesting
@@ -220,6 +221,19 @@
         }
     }
 
+    /**
+     * @return the device id for a given token (identifiying a device)
+     */
+    int getInputDeviceId(IBinder token) {
+        synchronized (mLock) {
+            final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(token);
+            if (inputDeviceDescriptor == null) {
+                throw new IllegalArgumentException("Could not get device id for given token");
+            }
+            return inputDeviceDescriptor.getInputDeviceId();
+        }
+    }
+
     void setShowPointerIcon(boolean visible, int displayId) {
         mInputManagerInternal.setPointerIconVisible(visible, displayId);
     }
@@ -393,10 +407,22 @@
                         + inputDeviceDescriptor.getCreationOrderNumber());
                 fout.println("          type: " + inputDeviceDescriptor.getType());
                 fout.println("          phys: " + inputDeviceDescriptor.getPhys());
+                fout.println(
+                        "          inputDeviceId: " + inputDeviceDescriptor.getInputDeviceId());
             }
         }
     }
 
+    @VisibleForTesting
+    void addDeviceForTesting(IBinder deviceToken, int fd, int type, int displayId,
+            String phys, int inputDeviceId) {
+        synchronized (mLock) {
+            mInputDeviceDescriptors.put(deviceToken,
+                    new InputDeviceDescriptor(fd, () -> {}, type, displayId, phys,
+                            inputDeviceId));
+        }
+    }
+
     private static native int nativeOpenUinputDpad(String deviceName, int vendorId,
             int productId, String phys);
     private static native int nativeOpenUinputKeyboard(String deviceName, int vendorId,
@@ -493,16 +519,20 @@
         private final @Type int mType;
         private final int mDisplayId;
         private final String mPhys;
+        // The input device id that was associated to the device by the InputReader on device
+        // creation.
+        private final int mInputDeviceId;
         // Monotonically increasing number; devices with lower numbers were created earlier.
         private final long mCreationOrderNumber;
 
         InputDeviceDescriptor(int fd, IBinder.DeathRecipient deathRecipient, @Type int type,
-                int displayId, String phys) {
+                int displayId, String phys, int inputDeviceId) {
             mFd = fd;
             mDeathRecipient = deathRecipient;
             mType = type;
             mDisplayId = displayId;
             mPhys = phys;
+            mInputDeviceId = inputDeviceId;
             mCreationOrderNumber = sNextCreationOrderNumber.getAndIncrement();
         }
 
@@ -533,6 +563,10 @@
         public String getPhys() {
             return mPhys;
         }
+
+        public int getInputDeviceId() {
+            return mInputDeviceId;
+        }
     }
 
     private final class BinderDeathRecipient implements IBinder.DeathRecipient {
@@ -558,6 +592,8 @@
         private final CountDownLatch mDeviceAddedLatch = new CountDownLatch(1);
         private final InputManager.InputDeviceListener mListener;
 
+        private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID;
+
         WaitForDevice(String deviceName, int vendorId, int productId) {
             mListener = new InputManager.InputDeviceListener() {
                 @Override
@@ -572,6 +608,7 @@
                     if (id.getVendorId() != vendorId || id.getProductId() != productId) {
                         return;
                     }
+                    mInputDeviceId = deviceId;
                     mDeviceAddedLatch.countDown();
                 }
 
@@ -588,8 +625,13 @@
             InputManager.getInstance().registerInputDeviceListener(mListener, mHandler);
         }
 
-        /** Note: This must not be called from {@link #mHandler}'s thread. */
-        void waitForDeviceCreation() throws DeviceCreationException {
+        /**
+         * Note: This must not be called from {@link #mHandler}'s thread.
+         * @throws DeviceCreationException if the device was not created successfully within the
+         * timeout.
+         * @return The id of the created input device.
+         */
+        int waitForDeviceCreation() throws DeviceCreationException {
             try {
                 if (!mDeviceAddedLatch.await(1, TimeUnit.MINUTES)) {
                     throw new DeviceCreationException(
@@ -599,6 +641,12 @@
                 throw new DeviceCreationException(
                         "Interrupted while waiting for virtual device to be created.", e);
             }
+            if (mInputDeviceId == IInputConstants.INVALID_INPUT_DEVICE_ID) {
+                throw new IllegalStateException(
+                        "Virtual input device was created with an invalid "
+                                + "id=" + mInputDeviceId);
+            }
+            return mInputDeviceId;
         }
 
         @Override
@@ -643,6 +691,8 @@
         final int fd;
         final BinderDeathRecipient binderDeathRecipient;
 
+        final int inputDeviceId;
+
         setUniqueIdAssociation(displayId, phys);
         try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) {
             fd = deviceOpener.get();
@@ -652,7 +702,7 @@
             }
             // The fd is valid from here, so ensure that all failures close the fd after this point.
             try {
-                waiter.waitForDeviceCreation();
+                inputDeviceId = waiter.waitForDeviceCreation();
 
                 binderDeathRecipient = new BinderDeathRecipient(deviceToken);
                 try {
@@ -672,7 +722,8 @@
 
         synchronized (mLock) {
             mInputDeviceDescriptors.put(deviceToken,
-                    new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys));
+                    new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys,
+                            inputDeviceId));
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 2835b69..5ebbf07 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -498,6 +498,17 @@
     }
 
     @Override // Binder call
+    public int getInputDeviceId(IBinder token) {
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            return mInputController.getInputDeviceId(token);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+
+    @Override // Binder call
     public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
         final long binderToken = Binder.clearCallingIdentity();
         try {
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index f7833b0..2652ebe 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2581,33 +2581,39 @@
         if (!checkNotifyPermission("notifyBarringInfo()")) {
             return;
         }
-        if (barringInfo == null) {
-            log("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId);
-            mBarringInfo.set(phoneId, new BarringInfo());
+        if (!validatePhoneId(phoneId)) {
+            loge("Received invalid phoneId for BarringInfo = " + phoneId);
             return;
         }
 
         synchronized (mRecords) {
-            if (validatePhoneId(phoneId)) {
-                mBarringInfo.set(phoneId, barringInfo);
-                // Barring info is non-null
-                BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy();
-                if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
-                for (Record r : mRecords) {
-                    if (r.matchTelephonyCallbackEvent(
-                            TelephonyCallback.EVENT_BARRING_INFO_CHANGED)
-                            && idMatch(r, subId, phoneId)) {
-                        try {
-                            if (DBG_LOC) {
-                                log("notifyBarringInfo: mBarringInfo="
-                                        + barringInfo + " r=" + r);
-                            }
-                            r.callback.onBarringInfoChanged(
-                                    checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
-                                        ? barringInfo : biNoLocation);
-                        } catch (RemoteException ex) {
-                            mRemoveList.add(r.binder);
+            if (barringInfo == null) {
+                loge("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId);
+                mBarringInfo.set(phoneId, new BarringInfo());
+                return;
+            }
+            if (barringInfo.equals(mBarringInfo.get(phoneId))) {
+                if (VDBG) log("Ignoring duplicate barring info.");
+                return;
+            }
+            mBarringInfo.set(phoneId, barringInfo);
+            // Barring info is non-null
+            BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy();
+            if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
+            for (Record r : mRecords) {
+                if (r.matchTelephonyCallbackEvent(
+                        TelephonyCallback.EVENT_BARRING_INFO_CHANGED)
+                        && idMatch(r, subId, phoneId)) {
+                    try {
+                        if (DBG_LOC) {
+                            log("notifyBarringInfo: mBarringInfo="
+                                    + barringInfo + " r=" + r);
                         }
+                        r.callback.onBarringInfoChanged(
+                                checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
+                                    ? barringInfo : biNoLocation);
+                    } catch (RemoteException ex) {
+                        mRemoveList.add(r.binder);
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 202f4775..5d46de3 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -152,6 +152,8 @@
 
     // flag set by resource, whether to start dream immediately upon docking even if unlocked.
     private boolean mStartDreamImmediatelyOnDock = true;
+    // flag set by resource, whether to disable dreams when ambient mode suppression is enabled.
+    private boolean mDreamsDisabledByAmbientModeSuppression = false;
     // flag set by resource, whether to enable Car dock launch when starting car mode.
     private boolean mEnableCarDockLaunch = true;
     // flag set by resource, whether to lock UI mode to the default one or not.
@@ -364,6 +366,11 @@
         mStartDreamImmediatelyOnDock = startDreamImmediatelyOnDock;
     }
 
+    @VisibleForTesting
+    void setDreamsDisabledByAmbientModeSuppression(boolean disabledByAmbientModeSuppression) {
+        mDreamsDisabledByAmbientModeSuppression = disabledByAmbientModeSuppression;
+    }
+
     @Override
     public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
         mCurrentUser = to.getUserIdentifier();
@@ -424,6 +431,8 @@
         final Resources res = context.getResources();
         mStartDreamImmediatelyOnDock = res.getBoolean(
                 com.android.internal.R.bool.config_startDreamImmediatelyOnDock);
+        mDreamsDisabledByAmbientModeSuppression = res.getBoolean(
+                com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig);
         mNightMode = res.getInteger(
                 com.android.internal.R.integer.config_defaultNightMode);
         mDefaultUiModeType = res.getInteger(
@@ -1827,10 +1836,14 @@
         // Send the new configuration.
         applyConfigurationExternallyLocked();
 
+        final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppression
+                && mLocalPowerManager.isAmbientDisplaySuppressed();
+
         // If we did not start a dock app, then start dreaming if appropriate.
-        if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock
-                || mWindowManager.isKeyguardShowingAndNotOccluded()
-                || !mPowerManager.isInteractive())) {
+        if (category != null && !dockAppStarted && !dreamsSuppressed && (
+                mStartDreamImmediatelyOnDock
+                        || mWindowManager.isKeyguardShowingAndNotOccluded()
+                        || !mPowerManager.isInteractive())) {
             mInjector.startDreamWhenDockedIfAppropriate(getContext());
         }
     }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 7a09109..82af12e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -3967,8 +3967,12 @@
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
+        final boolean hasKillAllPermission = checkCallingPermission(
+                android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES) == PERMISSION_GRANTED;
+        final int callingUid = Binder.getCallingUid();
+        final int callingAppId = UserHandle.getAppId(callingUid);
 
-        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
+        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid,
                 userId, true, ALLOW_FULL_ONLY, "killBackgroundProcesses", null);
         final int[] userIds = mUserController.expandUserId(userId);
 
@@ -3983,7 +3987,7 @@
                                     targetUserId));
                 } catch (RemoteException e) {
                 }
-                if (appId == -1) {
+                if (appId == -1 || (!hasKillAllPermission && appId != callingAppId)) {
                     Slog.w(TAG, "Invalid packageName: " + packageName);
                     return;
                 }
@@ -4002,11 +4006,11 @@
 
     @Override
     public void killAllBackgroundProcesses() {
-        if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES)
+        if (checkCallingPermission(android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES)
                 != PackageManager.PERMISSION_GRANTED) {
             final String msg = "Permission Denial: killAllBackgroundProcesses() from pid="
                     + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
-                    + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES;
+                    + " requires " + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES;
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
@@ -4042,11 +4046,11 @@
      *                     processes, or {@code -1} to ignore the process state
      */
     void killAllBackgroundProcessesExcept(int minTargetSdk, int maxProcState) {
-        if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES)
+        if (checkCallingPermission(android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES)
                 != PackageManager.PERMISSION_GRANTED) {
             final String msg = "Permission Denial: killAllBackgroundProcessesExcept() from pid="
                     + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
-                    + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES;
+                    + " requires " + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES;
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
@@ -13373,27 +13377,19 @@
         int callingPid;
         boolean instantApp;
         synchronized(this) {
-            if (caller != null) {
-                callerApp = getRecordForAppLOSP(caller);
-                if (callerApp == null) {
-                    throw new SecurityException(
-                            "Unable to find app for caller " + caller
-                            + " (pid=" + Binder.getCallingPid()
-                            + ") when registering receiver " + receiver);
-                }
-                if (callerApp.info.uid != SYSTEM_UID
-                        && !callerApp.getPkgList().containsKey(callerPackage)
-                        && !"android".equals(callerPackage)) {
-                    throw new SecurityException("Given caller package " + callerPackage
-                            + " is not running in process " + callerApp);
-                }
-                callingUid = callerApp.info.uid;
-                callingPid = callerApp.getPid();
-            } else {
-                callerPackage = null;
-                callingUid = Binder.getCallingUid();
-                callingPid = Binder.getCallingPid();
+            callerApp = getRecordForAppLOSP(caller);
+            if (callerApp == null) {
+                Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller);
+                return null;
             }
+            if (callerApp.info.uid != SYSTEM_UID
+                    && !callerApp.getPkgList().containsKey(callerPackage)
+                    && !"android".equals(callerPackage)) {
+                throw new SecurityException("Given caller package " + callerPackage
+                        + " is not running in process " + callerApp);
+            }
+            callingUid = callerApp.info.uid;
+            callingPid = callerApp.getPid();
 
             instantApp = isInstantApp(callerApp, callerPackage, callingUid);
             userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
@@ -14700,13 +14696,14 @@
             // Non-system callers can't declare that a broadcast is alarm-related.
             // The PendingIntent invocation case is handled in PendingIntentRecord.
             if (bOptions != null && callingUid != SYSTEM_UID) {
-                if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
+                if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)
+                        || bOptions.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) {
                     if (DEBUG_BROADCAST) {
                         Slog.w(TAG, "Non-system caller " + callingUid
-                                + " may not flag broadcast as alarm-related");
+                                + " may not flag broadcast as alarm or interactive");
                     }
                     throw new SecurityException(
-                            "Non-system callers may not flag broadcasts as alarm-related");
+                            "Non-system callers may not flag broadcasts as alarm or interactive");
                 }
             }
 
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index a4a1c2f..28a81e6 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -167,7 +167,7 @@
      */
     public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS;
     private static final String KEY_DELAY_NORMAL_MILLIS = "bcast_delay_normal_millis";
-    private static final long DEFAULT_DELAY_NORMAL_MILLIS = 10_000 * Build.HW_TIMEOUT_MULTIPLIER;
+    private static final long DEFAULT_DELAY_NORMAL_MILLIS = 1_000;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts
@@ -175,7 +175,7 @@
      */
     public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS;
     private static final String KEY_DELAY_CACHED_MILLIS = "bcast_delay_cached_millis";
-    private static final long DEFAULT_DELAY_CACHED_MILLIS = 30_000 * Build.HW_TIMEOUT_MULTIPLIER;
+    private static final long DEFAULT_DELAY_CACHED_MILLIS = 10_000;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of complete
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 0d6ac1d..868c3ae 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -103,6 +103,13 @@
     private final ArrayDeque<SomeArgs> mPending = new ArrayDeque<>();
 
     /**
+     * Ordered collection of "urgent" broadcasts that are waiting to be
+     * dispatched to this process, in the same representation as
+     * {@link #mPending}.
+     */
+    private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>();
+
+    /**
      * Broadcast actively being dispatched to this process.
      */
     private @Nullable BroadcastRecord mActive;
@@ -140,12 +147,16 @@
     private int mCountOrdered;
     private int mCountAlarm;
     private int mCountPrioritized;
+    private int mCountInteractive;
+    private int mCountResultTo;
+    private int mCountInstrumented;
 
     private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE;
     private @Reason int mRunnableAtReason = REASON_EMPTY;
     private boolean mRunnableAtInvalidated;
 
     private boolean mProcessCached;
+    private boolean mProcessInstrumented;
 
     private String mCachedToString;
     private String mCachedToShortString;
@@ -172,40 +183,65 @@
      */
     public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
             int blockedUntilTerminalCount) {
-        // If caller wants to replace, walk backwards looking for any matches
         if (record.isReplacePending()) {
-            final Iterator<SomeArgs> it = mPending.descendingIterator();
-            final Object receiver = record.receivers.get(recordIndex);
-            while (it.hasNext()) {
-                final SomeArgs args = it.next();
-                final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
-                final Object testReceiver = testRecord.receivers.get(args.argi1);
-                if ((record.callingUid == testRecord.callingUid)
-                        && (record.userId == testRecord.userId)
-                        && record.intent.filterEquals(testRecord.intent)
-                        && isReceiverEquals(receiver, testReceiver)) {
-                    // Exact match found; perform in-place swap
-                    args.arg1 = record;
-                    args.argi1 = recordIndex;
-                    args.argi2 = blockedUntilTerminalCount;
-                    onBroadcastDequeued(testRecord);
-                    onBroadcastEnqueued(record);
-                    return;
-                }
+            boolean didReplace = replaceBroadcastInQueue(mPending,
+                    record, recordIndex, blockedUntilTerminalCount)
+                    || replaceBroadcastInQueue(mPendingUrgent,
+                    record, recordIndex, blockedUntilTerminalCount);
+            if (didReplace) {
+                return;
             }
         }
 
         // Caller isn't interested in replacing, or we didn't find any pending
         // item to replace above, so enqueue as a new broadcast
-        SomeArgs args = SomeArgs.obtain();
-        args.arg1 = record;
-        args.argi1 = recordIndex;
-        args.argi2 = blockedUntilTerminalCount;
-        mPending.addLast(args);
+        SomeArgs newBroadcastArgs = SomeArgs.obtain();
+        newBroadcastArgs.arg1 = record;
+        newBroadcastArgs.argi1 = recordIndex;
+        newBroadcastArgs.argi2 = blockedUntilTerminalCount;
+
+        // Cross-broadcast prioritization policy:  some broadcasts might warrant being
+        // issued ahead of others that are already pending, for example if this new
+        // broadcast is in a different delivery class or is tied to a direct user interaction
+        // with implicit responsiveness expectations.
+        final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending;
+        queue.addLast(newBroadcastArgs);
         onBroadcastEnqueued(record);
     }
 
     /**
+     * Searches from newest to oldest, and at the first matching pending broadcast
+     * it finds, replaces it in-place and returns -- does not attempt to handle
+     * "duplicate" broadcasts in the queue.
+     * <p>
+     * @return {@code true} if it found and replaced an existing record in the queue;
+     * {@code false} otherwise.
+     */
+    private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
+            @NonNull BroadcastRecord record, int recordIndex,  int blockedUntilTerminalCount) {
+        final Iterator<SomeArgs> it = queue.descendingIterator();
+        final Object receiver = record.receivers.get(recordIndex);
+        while (it.hasNext()) {
+            final SomeArgs args = it.next();
+            final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
+            final Object testReceiver = testRecord.receivers.get(args.argi1);
+            if ((record.callingUid == testRecord.callingUid)
+                    && (record.userId == testRecord.userId)
+                    && record.intent.filterEquals(testRecord.intent)
+                    && isReceiverEquals(receiver, testReceiver)) {
+                // Exact match found; perform in-place swap
+                args.arg1 = record;
+                args.argi1 = recordIndex;
+                args.argi2 = blockedUntilTerminalCount;
+                onBroadcastDequeued(testRecord);
+                onBroadcastEnqueued(record);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Functional interface that tests a {@link BroadcastRecord} that has been
      * previously enqueued in {@link BroadcastProcessQueue}.
      */
@@ -233,8 +269,18 @@
      */
     public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate,
             @NonNull BroadcastConsumer consumer, boolean andRemove) {
+        boolean didSomething = forEachMatchingBroadcastInQueue(mPending,
+                predicate, consumer, andRemove);
+        didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent,
+                predicate, consumer, andRemove);
+        return didSomething;
+    }
+
+    private boolean forEachMatchingBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
+            @NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer,
+            boolean andRemove) {
         boolean didSomething = false;
-        final Iterator<SomeArgs> it = mPending.iterator();
+        final Iterator<SomeArgs> it = queue.iterator();
         while (it.hasNext()) {
             final SomeArgs args = it.next();
             final BroadcastRecord record = (BroadcastRecord) args.arg1;
@@ -255,6 +301,18 @@
     }
 
     /**
+     * Update the actively running "warm" process for this process.
+     */
+    public void setProcess(@Nullable ProcessRecord app) {
+        this.app = app;
+        if (app != null) {
+            setProcessInstrumented(app.getActiveInstrumentation() != null);
+        } else {
+            setProcessInstrumented(false);
+        }
+    }
+
+    /**
      * Update if this process is in the "cached" state, typically signaling that
      * broadcast dispatch should be paused or delayed.
      */
@@ -266,6 +324,18 @@
     }
 
     /**
+     * Update if this process is in the "instrumented" state, typically
+     * signaling that broadcast dispatch should bypass all pauses or delays, to
+     * avoid holding up test suites.
+     */
+    public void setProcessInstrumented(boolean instrumented) {
+        if (mProcessInstrumented != instrumented) {
+            mProcessInstrumented = instrumented;
+            invalidateRunnableAt();
+        }
+    }
+
+    /**
      * Return if we know of an actively running "warm" process for this queue.
      */
     public boolean isProcessWarm() {
@@ -273,13 +343,12 @@
     }
 
     public int getPreferredSchedulingGroupLocked() {
-        if (mCountForeground > 0 || mCountOrdered > 0 || mCountAlarm > 0) {
-            // We have an important broadcast somewhere down the queue, so
+        if (mCountForeground > 0) {
+            // We have a foreground broadcast somewhere down the queue, so
             // boost priority until we drain them all
             return ProcessList.SCHED_GROUP_DEFAULT;
-        } else if ((mActive != null)
-                && (mActive.isForeground() || mActive.ordered || mActive.alarm)) {
-            // We have an important broadcast right now, so boost priority
+        } else if ((mActive != null) && mActive.isForeground()) {
+            // We have a foreground broadcast right now, so boost priority
             return ProcessList.SCHED_GROUP_DEFAULT;
         } else if (!isIdle()) {
             return ProcessList.SCHED_GROUP_BACKGROUND;
@@ -309,7 +378,7 @@
      */
     public void makeActiveNextPending() {
         // TODO: what if the next broadcast isn't runnable yet?
-        final SomeArgs next = mPending.removeFirst();
+        final SomeArgs next = removeNextBroadcast();
         mActive = (BroadcastRecord) next.arg1;
         mActiveIndex = next.argi1;
         mActiveBlockedUntilTerminalCount = next.argi2;
@@ -347,6 +416,15 @@
         if (record.prioritized) {
             mCountPrioritized++;
         }
+        if (record.interactive) {
+            mCountInteractive++;
+        }
+        if (record.resultTo != null) {
+            mCountResultTo++;
+        }
+        if (record.callerInstrumented) {
+            mCountInstrumented++;
+        }
         invalidateRunnableAt();
     }
 
@@ -366,6 +444,15 @@
         if (record.prioritized) {
             mCountPrioritized--;
         }
+        if (record.interactive) {
+            mCountInteractive--;
+        }
+        if (record.resultTo != null) {
+            mCountResultTo--;
+        }
+        if (record.callerInstrumented) {
+            mCountInstrumented--;
+        }
         invalidateRunnableAt();
     }
 
@@ -413,7 +500,7 @@
     }
 
     public boolean isEmpty() {
-        return mPending.isEmpty();
+        return mPending.isEmpty() && mPendingUrgent.isEmpty();
     }
 
     public boolean isActive() {
@@ -421,6 +508,38 @@
     }
 
     /**
+     * Will thrown an exception if there are no pending broadcasts; relies on
+     * {@link #isEmpty()} being false.
+     */
+    SomeArgs removeNextBroadcast() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return queue.removeFirst();
+    }
+
+    @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() {
+        if (!mPendingUrgent.isEmpty()) {
+            return mPendingUrgent;
+        } else if (!mPending.isEmpty()) {
+            return mPending;
+        }
+        return null;
+    }
+
+    /**
+     * Returns null if there are no pending broadcasts
+     */
+    @Nullable SomeArgs peekNextBroadcast() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return (queue != null) ? queue.peekFirst() : null;
+    }
+
+    @VisibleForTesting
+    @Nullable BroadcastRecord peekNextBroadcastRecord() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return (queue != null) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
+    }
+
+    /**
      * Quickly determine if this queue has broadcasts that are still waiting to
      * be delivered at some point in the future.
      */
@@ -437,11 +556,13 @@
             return mActive.enqueueTime > barrierTime;
         }
         final SomeArgs next = mPending.peekFirst();
-        if (next != null) {
-            return ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
-        }
-        // Nothing running or runnable means we're past the barrier
-        return true;
+        final SomeArgs nextUrgent = mPendingUrgent.peekFirst();
+        // Empty queue is past any barrier
+        final boolean nextLater = next == null
+                || ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
+        final boolean nextUrgentLater = nextUrgent == null
+                || ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime;
+        return nextLater && nextUrgentLater;
     }
 
     public boolean isRunnable() {
@@ -477,25 +598,33 @@
     }
 
     static final int REASON_EMPTY = 0;
-    static final int REASON_CONTAINS_FOREGROUND = 1;
-    static final int REASON_CONTAINS_ORDERED = 2;
-    static final int REASON_CONTAINS_ALARM = 3;
-    static final int REASON_CONTAINS_PRIORITIZED = 4;
-    static final int REASON_CACHED = 5;
-    static final int REASON_NORMAL = 6;
-    static final int REASON_MAX_PENDING = 7;
-    static final int REASON_BLOCKED = 8;
+    static final int REASON_CACHED = 1;
+    static final int REASON_NORMAL = 2;
+    static final int REASON_MAX_PENDING = 3;
+    static final int REASON_BLOCKED = 4;
+    static final int REASON_INSTRUMENTED = 5;
+    static final int REASON_CONTAINS_FOREGROUND = 10;
+    static final int REASON_CONTAINS_ORDERED = 11;
+    static final int REASON_CONTAINS_ALARM = 12;
+    static final int REASON_CONTAINS_PRIORITIZED = 13;
+    static final int REASON_CONTAINS_INTERACTIVE = 14;
+    static final int REASON_CONTAINS_RESULT_TO = 15;
+    static final int REASON_CONTAINS_INSTRUMENTED = 16;
 
     @IntDef(flag = false, prefix = { "REASON_" }, value = {
             REASON_EMPTY,
-            REASON_CONTAINS_FOREGROUND,
-            REASON_CONTAINS_ORDERED,
-            REASON_CONTAINS_ALARM,
-            REASON_CONTAINS_PRIORITIZED,
             REASON_CACHED,
             REASON_NORMAL,
             REASON_MAX_PENDING,
             REASON_BLOCKED,
+            REASON_INSTRUMENTED,
+            REASON_CONTAINS_FOREGROUND,
+            REASON_CONTAINS_ORDERED,
+            REASON_CONTAINS_ALARM,
+            REASON_CONTAINS_PRIORITIZED,
+            REASON_CONTAINS_INTERACTIVE,
+            REASON_CONTAINS_RESULT_TO,
+            REASON_CONTAINS_INSTRUMENTED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Reason {}
@@ -503,14 +632,18 @@
     static @NonNull String reasonToString(@Reason int reason) {
         switch (reason) {
             case REASON_EMPTY: return "EMPTY";
-            case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND";
-            case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED";
-            case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM";
-            case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED";
             case REASON_CACHED: return "CACHED";
             case REASON_NORMAL: return "NORMAL";
             case REASON_MAX_PENDING: return "MAX_PENDING";
             case REASON_BLOCKED: return "BLOCKED";
+            case REASON_INSTRUMENTED: return "INSTRUMENTED";
+            case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND";
+            case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED";
+            case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM";
+            case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED";
+            case REASON_CONTAINS_INTERACTIVE: return "CONTAINS_INTERACTIVE";
+            case REASON_CONTAINS_RESULT_TO: return "CONTAINS_RESULT_TO";
+            case REASON_CONTAINS_INSTRUMENTED: return "CONTAINS_INSTRUMENTED";
             default: return Integer.toString(reason);
         }
     }
@@ -519,7 +652,7 @@
      * Update {@link #getRunnableAt()} if it's currently invalidated.
      */
     private void updateRunnableAt() {
-        final SomeArgs next = mPending.peekFirst();
+        final SomeArgs next = peekNextBroadcast();
         if (next != null) {
             final BroadcastRecord r = (BroadcastRecord) next.arg1;
             final int index = next.argi1;
@@ -537,7 +670,7 @@
 
             // If we have too many broadcasts pending, bypass any delays that
             // might have been applied above to aid draining
-            if (mPending.size() >= constants.MAX_PENDING_BROADCASTS) {
+            if (mPending.size() + mPendingUrgent.size() >= constants.MAX_PENDING_BROADCASTS) {
                 mRunnableAt = runnableAt;
                 mRunnableAtReason = REASON_MAX_PENDING;
                 return;
@@ -555,6 +688,18 @@
             } else if (mCountPrioritized > 0) {
                 mRunnableAt = runnableAt;
                 mRunnableAtReason = REASON_CONTAINS_PRIORITIZED;
+            } else if (mCountInteractive > 0) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_CONTAINS_INTERACTIVE;
+            } else if (mCountResultTo > 0) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_CONTAINS_RESULT_TO;
+            } else if (mCountInstrumented > 0) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_CONTAINS_INSTRUMENTED;
+            } else if (mProcessInstrumented) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_INSTRUMENTED;
             } else if (mProcessCached) {
                 mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS;
                 mRunnableAtReason = REASON_CACHED;
@@ -574,8 +719,8 @@
      */
     public void checkHealthLocked() {
         if (mRunnableAtReason == REASON_BLOCKED) {
-            final SomeArgs next = mPending.peekFirst();
-            Objects.requireNonNull(next, "peekFirst");
+            final SomeArgs next = peekNextBroadcast();
+            Objects.requireNonNull(next, "peekNextBroadcast");
 
             // If blocked more than 10 minutes, we're likely wedged
             final BroadcastRecord r = (BroadcastRecord) next.arg1;
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 1e1ebeb..db3ef3d 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -44,6 +44,7 @@
 import android.annotation.UptimeMillisLong;
 import android.app.Activity;
 import android.app.ActivityManager;
+import android.app.BroadcastOptions;
 import android.app.IApplicationThread;
 import android.app.RemoteServiceException.CannotDeliverBroadcastException;
 import android.app.UidObserver;
@@ -440,7 +441,7 @@
         // relevant per-process queue
         final BroadcastProcessQueue queue = getProcessQueue(app);
         if (queue != null) {
-            queue.app = app;
+            queue.setProcess(app);
         }
 
         boolean didSomething = false;
@@ -477,7 +478,7 @@
         // relevant per-process queue
         final BroadcastProcessQueue queue = getProcessQueue(app);
         if (queue != null) {
-            queue.app = null;
+            queue.setProcess(null);
         }
 
         if ((mRunningColdStart != null) && (mRunningColdStart == queue)) {
@@ -534,6 +535,17 @@
             }, mBroadcastConsumerSkipAndCanceled, true);
         }
 
+        final int policy = (r.options != null)
+                ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL;
+        if (policy == BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) {
+            forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> {
+                // We only allow caller to remove broadcasts they enqueued
+                return (r.callingUid == testRecord.callingUid)
+                        && (r.userId == testRecord.userId)
+                        && r.matchesDeliveryGroup(testRecord);
+            }, mBroadcastConsumerSkipAndCanceled, true);
+        }
+
         if (r.isReplacePending()) {
             // Leave the skipped broadcasts intact in queue, so that we can
             // replace them at their current position during enqueue below
@@ -804,19 +816,21 @@
         }
 
         final BroadcastRecord r = queue.getActive();
-        r.resultCode = resultCode;
-        r.resultData = resultData;
-        r.resultExtras = resultExtras;
-        if (!r.isNoAbort()) {
-            r.resultAbort = resultAbort;
-        }
+        if (r.ordered) {
+            r.resultCode = resultCode;
+            r.resultData = resultData;
+            r.resultExtras = resultExtras;
+            if (!r.isNoAbort()) {
+                r.resultAbort = resultAbort;
+            }
 
-        // When the caller aborted an ordered broadcast, we mark all remaining
-        // receivers as skipped
-        if (r.ordered && r.resultAbort) {
-            for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) {
-                setDeliveryState(null, null, r, i, r.receivers.get(i),
-                        BroadcastRecord.DELIVERY_SKIPPED);
+            // When the caller aborted an ordered broadcast, we mark all
+            // remaining receivers as skipped
+            if (r.resultAbort) {
+                for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) {
+                    setDeliveryState(null, null, r, i, r.receivers.get(i),
+                            BroadcastRecord.DELIVERY_SKIPPED);
+                }
             }
         }
 
@@ -913,7 +927,8 @@
             notifyFinishReceiver(queue, r, index, receiver);
 
             // When entire ordered broadcast finished, deliver final result
-            if (r.ordered && (r.terminalCount == r.receivers.size())) {
+            final boolean recordFinished = (r.terminalCount == r.receivers.size());
+            if (recordFinished) {
                 scheduleResultTo(r);
             }
 
@@ -1205,7 +1220,7 @@
 
     private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) {
         if (!queue.isProcessWarm()) {
-            queue.app = mService.getProcessRecordLocked(queue.processName, queue.uid);
+            queue.setProcess(mService.getProcessRecordLocked(queue.processName, queue.uid));
         }
     }
 
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 2d82595..d7dc8b8 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -78,11 +78,13 @@
     final int callingPid;   // the pid of who sent this
     final int callingUid;   // the uid of who sent this
     final boolean callerInstantApp; // caller is an Instant App?
+    final boolean callerInstrumented; // caller is being instrumented
     final boolean ordered;  // serialize the send to receivers?
     final boolean sticky;   // originated from existing sticky data?
     final boolean alarm;    // originated from an alarm triggering?
     final boolean pushMessage; // originated from a push message?
     final boolean pushMessageOverQuota; // originated from a push message which was over quota?
+    final boolean interactive; // originated from user interaction?
     final boolean initialSticky; // initial broadcast from register to sticky?
     final boolean prioritized; // contains more than one priority tranche
     final int userId;       // user id this broadcast was for
@@ -364,6 +366,8 @@
         callingPid = _callingPid;
         callingUid = _callingUid;
         callerInstantApp = _callerInstantApp;
+        callerInstrumented = (_callerApp != null)
+                ? (_callerApp.getActiveInstrumentation() != null) : false;
         resolvedType = _resolvedType;
         requiredPermissions = _requiredPermissions;
         excludedPermissions = _excludedPermissions;
@@ -392,6 +396,7 @@
         alarm = options != null && options.isAlarmBroadcast();
         pushMessage = options != null && options.isPushMessagingBroadcast();
         pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast();
+        interactive = options != null && options.isInteractiveBroadcast();
         this.filterExtrasForReceiver = filterExtrasForReceiver;
     }
 
@@ -409,6 +414,7 @@
         callingPid = from.callingPid;
         callingUid = from.callingUid;
         callerInstantApp = from.callerInstantApp;
+        callerInstrumented = from.callerInstrumented;
         ordered = from.ordered;
         sticky = from.sticky;
         initialSticky = from.initialSticky;
@@ -450,6 +456,7 @@
         alarm = from.alarm;
         pushMessage = from.pushMessage;
         pushMessageOverQuota = from.pushMessageOverQuota;
+        interactive = from.interactive;
         filterExtrasForReceiver = from.filterExtrasForReceiver;
     }
 
@@ -611,6 +618,18 @@
         return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0;
     }
 
+    /**
+     * Core policy determination about this broadcast's delivery prioritization
+     */
+    boolean isUrgent() {
+        // TODO: flags for controlling policy
+        // TODO: migrate alarm-prioritization flag to BroadcastConstants
+        return (isForeground()
+                || interactive
+                || alarm)
+                && receivers.size() == 1;
+    }
+
     @NonNull String getHostingRecordTriggerType() {
         if (alarm) {
             return HostingRecord.TRIGGER_TYPE_ALARM;
@@ -796,6 +815,16 @@
         }
     }
 
+    public boolean matchesDeliveryGroup(@NonNull BroadcastRecord other) {
+        final String key = (options != null) ? options.getDeliveryGroupKey() : null;
+        final String otherKey = (other.options != null)
+                ? other.options.getDeliveryGroupKey() : null;
+        if (key == null && otherKey == null) {
+            return intent.filterEquals(other.intent);
+        }
+        return Objects.equals(key, otherKey);
+    }
+
     @Override
     public String toString() {
         if (mCachedToString == null) {
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 975619f..740efbc 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -443,13 +443,14 @@
         // invocation side effects such as allowlisting.
         if (options != null && callingUid != Process.SYSTEM_UID
                 && key.type == ActivityManager.INTENT_SENDER_BROADCAST) {
-            if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
+            if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)
+                    || options.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) {
                 if (DEBUG_BROADCAST_LIGHT) {
                     Slog.w(TAG, "Non-system caller " + callingUid
-                            + " may not flag broadcast as alarm-related");
+                            + " may not flag broadcast as alarm or interactive");
                 }
                 throw new SecurityException(
-                        "Non-system callers may not flag broadcasts as alarm-related");
+                        "Non-system callers may not flag broadcasts as alarm or interactive");
             }
         }
 
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 216a48e..3fa41c0 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -16,7 +16,6 @@
 
 package com.android.server.am;
 
-import static android.Manifest.permission.CREATE_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
@@ -1482,7 +1481,7 @@
     // defined
     boolean startUserOnSecondaryDisplay(@UserIdInt int userId, int displayId) {
         checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay",
-                MANAGE_USERS, CREATE_USERS);
+                MANAGE_USERS, INTERACT_ACROSS_USERS);
 
         // DEFAULT_DISPLAY is used for the current foreground user only
         Preconditions.checkArgument(displayId != Display.DEFAULT_DISPLAY,
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
index dec1b55..5bc9d23 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
@@ -54,7 +54,7 @@
     private AuthResultCoordinator mAuthResultCoordinator;
 
     public AuthSessionCoordinator() {
-        this(SystemClock.currentNetworkTimeClock());
+        this(SystemClock.elapsedRealtimeClock());
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
index 1370fd8..da7781a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
@@ -21,6 +21,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.biometrics.BiometricConstants;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -293,4 +294,30 @@
                 + ", requestId=" + getRequestId()
                 + ", userId=" + getTargetUserId() + "}";
     }
+
+    /**
+     * Cancels this ClientMonitor
+     */
+    public void cancel() {
+        cancelWithoutStarting(mCallback);
+    }
+
+    /**
+     * Cancels this ClientMonitor without starting
+     * @param callback
+     */
+    public void cancelWithoutStarting(@NonNull ClientMonitorCallback callback) {
+        Slog.d(TAG, "cancelWithoutStarting: " + this);
+
+        final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
+        try {
+            ClientMonitorCallbackConverter listener = getListener();
+            if (listener != null) {
+                listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
+            }
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to invoke sendError", e);
+        }
+        callback.onClientFinished(this, true /* success */);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index 9317c4e..fb978b2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -543,4 +543,37 @@
         mPendingOperations.clear();
         mCurrentOperation = null;
     }
+
+    /**
+     * Marks all pending operations as canceling and cancels the current
+     * operation.
+     */
+    private void clearScheduler() {
+        if (mCurrentOperation == null) {
+            return;
+        }
+        for (BiometricSchedulerOperation pendingOperation : mPendingOperations) {
+            Slog.d(getTag(), "[Watchdog cancelling pending] "
+                    + pendingOperation.getClientMonitor());
+            pendingOperation.markCanceling();
+        }
+        Slog.d(getTag(), "[Watchdog cancelling current] "
+                + mCurrentOperation.getClientMonitor());
+        mCurrentOperation.cancel(mHandler, getInternalCallback());
+    }
+
+    /**
+     * Start the timeout for the watchdog.
+     */
+    public void startWatchdog() {
+        if (mCurrentOperation == null) {
+            return;
+        }
+        final BiometricSchedulerOperation mOperation = mCurrentOperation;
+        mHandler.postDelayed(() -> {
+            if (mOperation == mCurrentOperation) {
+                clearScheduler();
+            }
+        }, 10000);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index ef2931f..dacec38 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -267,7 +267,7 @@
 
     /** Flags this operation as canceled, if possible, but does not cancel it until started. */
     public boolean markCanceling() {
-        if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) {
+        if (mState == STATE_WAITING_IN_QUEUE) {
             mState = STATE_WAITING_IN_QUEUE_CANCELING;
             return true;
         }
@@ -287,10 +287,6 @@
         }
 
         final int currentState = mState;
-        if (!isInterruptable()) {
-            Slog.w(TAG, "Cannot cancel - operation not interruptable: " + this);
-            return;
-        }
         if (currentState == STATE_STARTED_CANCELING) {
             Slog.w(TAG, "Cannot cancel - already invoked for operation: " + this);
             return;
@@ -301,10 +297,10 @@
                 || currentState == STATE_WAITING_IN_QUEUE_CANCELING
                 || currentState == STATE_WAITING_FOR_COOKIE) {
             Slog.d(TAG, "[Cancelling] Current client (without start): " + mClientMonitor);
-            ((Interruptable) mClientMonitor).cancelWithoutStarting(getWrappedCallback(callback));
+            mClientMonitor.cancelWithoutStarting(getWrappedCallback(callback));
         } else {
             Slog.d(TAG, "[Cancelling] Current client: " + mClientMonitor);
-            ((Interruptable) mClientMonitor).cancel();
+            mClientMonitor.cancel();
         }
 
         // forcibly finish this client if the HAL does not acknowledge within the timeout
diff --git a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
index d9bd04d..6605d49 100644
--- a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
@@ -22,7 +22,6 @@
 import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
 
 import android.hardware.biometrics.BiometricManager;
-import android.os.SystemClock;
 import android.util.Slog;
 
 import java.time.Clock;
@@ -43,10 +42,6 @@
     private final Map<Integer, Map<Integer, AuthenticatorState>> mCanUserAuthenticate;
     private final Clock mClock;
 
-    MultiBiometricLockoutState() {
-        this(SystemClock.currentNetworkTimeClock());
-    }
-
     MultiBiometricLockoutState(Clock clock) {
         mCanUserAuthenticate = new HashMap<>();
         mClock = clock;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 271bce9..2761ec0 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -183,6 +183,18 @@
                     receiver, opPackageName, disabledFeatures, previewSurface, debugConsent);
         }
 
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void scheduleWatchdog() {
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+            if (provider == null) {
+                Slog.w(TAG, "Null provider for scheduling watchdog");
+                return;
+            }
+
+            provider.second.scheduleWatchdog(provider.first);
+        }
+
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC)
         @Override // Binder call
         public long enrollRemotely(int userId, final IBinder token, final byte[] hardwareAuthToken,
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
index 4efaedb..85f95ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
@@ -128,4 +128,10 @@
             @NonNull String opPackageName);
 
     void dumpHal(int sensorId, @NonNull FileDescriptor fd, @NonNull String[] args);
+
+    /**
+     * Schedules watchdog for canceling hung operations
+     * @param sensorId sensor ID of the associated operation
+     */
+    default void scheduleWatchdog(int sensorId) {}
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index b60f9d8..c12994c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -52,6 +52,7 @@
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
+import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.InvalidationRequesterClient;
@@ -661,4 +662,14 @@
     void setTestHalEnabled(boolean enabled) {
         mTestHalEnabled = enabled;
     }
+
+    @Override
+    public void scheduleWatchdog(int sensorId) {
+        Slog.d(getTag(), "Starting watchdog for face");
+        final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler();
+        if (biometricScheduler == null) {
+            return;
+        }
+        biometricScheduler.startWatchdog();
+    }
 }
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 7e2742e..b0dc28d 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
@@ -879,6 +879,18 @@
                 provider.onPowerPressed();
             }
         }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void scheduleWatchdog() {
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+            if (provider == null) {
+                Slog.w(TAG, "Null provider for scheduling watchdog");
+                return;
+            }
+
+            provider.second.scheduleWatchdog(provider.first);
+        }
     };
 
     public FingerprintService(Context context) {
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 9075e7e..0c29f56 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
@@ -140,4 +140,10 @@
     @NonNull
     ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
             @NonNull String opPackageName);
+
+    /**
+     * Schedules watchdog for canceling hung operations
+     * @param sensorId sensor ID of the associated operation
+     */
+    default void scheduleWatchdog(int sensorId) {}
 }
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 f599acac..2e5663d 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
@@ -16,6 +16,7 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
 
 import android.annotation.NonNull;
@@ -59,6 +60,7 @@
 import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.function.Supplier;
 
@@ -92,7 +94,9 @@
     private long mWaitForAuthKeyguard;
     private long mWaitForAuthBp;
     private long mIgnoreAuthFor;
+    private long mSideFpsLastAcquireStartTime;
     private Runnable mAuthSuccessRunnable;
+    private final Clock mClock;
 
     FingerprintAuthenticationClient(
             @NonNull Context context,
@@ -117,7 +121,8 @@
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull Handler handler,
-            @Authenticators.Types int biometricStrength) {
+            @Authenticators.Types int biometricStrength,
+            @NonNull Clock clock) {
         super(
                 context,
                 lazyDaemon,
@@ -161,6 +166,8 @@
                         R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage);
         mBiometricStrength = biometricStrength;
         mAuthSessionCoordinator = biometricContext.getAuthSessionCoordinator();
+        mSideFpsLastAcquireStartTime = -1;
+        mClock = clock;
 
         if (mSensorProps.isAnySidefpsType()) {
             if (Build.isDebuggable()) {
@@ -246,8 +253,14 @@
                             return;
                         }
                         delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
-                        Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps waiting for power for: "
-                                + delay + "ms");
+
+                        if (mSideFpsLastAcquireStartTime != -1) {
+                            delay = Math.max(0,
+                                    delay - (mClock.millis() - mSideFpsLastAcquireStartTime));
+                        }
+
+                        Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps "
+                                + "waiting for power until: " + delay + "ms");
                     }
 
                     if (mHandler.hasMessages(MESSAGE_FINGER_UP)) {
@@ -271,13 +284,15 @@
         mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo));
         super.onAcquired(acquiredInfo, vendorCode);
         if (mSensorProps.isAnySidefpsType()) {
+            if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
+                mSideFpsLastAcquireStartTime = mClock.millis();
+            }
             final boolean shouldLookForVendor =
                     mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR;
             final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage;
             final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage;
             final boolean ignorePowerPress =
-                    (acquireMessageMatch && !shouldLookForVendor) || (shouldLookForVendor
-                            && acquireMessageMatch && vendorMessageMatch);
+                    acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch);
 
             if (ignorePowerPress) {
                 Slog.d(TAG, "(sideFPS) onFingerUp");
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 774aff1..17ba07f 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
@@ -47,6 +47,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
 import android.os.UserManager;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -58,6 +59,7 @@
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
+import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -449,7 +451,8 @@
                     mTaskStackListener, mSensors.get(sensorId).getLockoutCache(),
                     mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication,
                     mSensors.get(sensorId).getSensorProperties(), mHandler,
-                    Utils.getCurrentStrength(sensorId));
+                    Utils.getCurrentStrength(sensorId),
+                    SystemClock.elapsedRealtimeClock());
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
     }
@@ -777,4 +780,14 @@
         }
         return null;
     }
+
+    @Override
+    public void scheduleWatchdog(int sensorId) {
+        Slog.d(getTag(), "Starting watchdog for fingerprint");
+        final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler();
+        if (biometricScheduler == null) {
+            return;
+        }
+        biometricScheduler.startWatchdog();
+    }
 }
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 0770062..6a01042 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -29,6 +29,8 @@
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl;
 import com.android.server.utils.Slogf;
 
 import java.io.FileDescriptor;
@@ -47,7 +49,7 @@
     private static final List<String> SERVICE_NAMES = Arrays.asList(
             IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab");
 
-    private final com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl mHalAidl;
+    private final BroadcastRadioServiceImpl mHalAidl;
     private final BroadcastRadioService mService;
 
     /**
@@ -65,10 +67,15 @@
     }
 
     IRadioServiceAidlImpl(BroadcastRadioService service, ArrayList<String> serviceList) {
+        this(service, new BroadcastRadioServiceImpl(serviceList));
         Slogf.i(TAG, "Initialize BroadcastRadioServiceAidl(%s)", service);
-        mService = Objects.requireNonNull(service);
-        mHalAidl =
-                new com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl(serviceList);
+    }
+
+    @VisibleForTesting
+    IRadioServiceAidlImpl(BroadcastRadioService service, BroadcastRadioServiceImpl halAidl) {
+        mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
+        mHalAidl = Objects.requireNonNull(halAidl,
+                "Broadcast radio service implementation for AIDL HAL cannot be null");
     }
 
     @Override
@@ -96,8 +103,8 @@
         if (isDebugEnabled()) {
             Slogf.d(TAG, "Adding announcement listener for %s", Arrays.toString(enabledTypes));
         }
-        Objects.requireNonNull(enabledTypes);
-        Objects.requireNonNull(listener);
+        Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null");
+        Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
         return mHalAidl.addAnnouncementListener(enabledTypes, listener);
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 28b6d02..a8e4034 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.broadcastradio.hal2.AnnouncementAggregator;
 
 import java.io.FileDescriptor;
@@ -53,7 +54,7 @@
     private final List<RadioManager.ModuleProperties> mV1Modules;
 
     IRadioServiceHidlImpl(BroadcastRadioService service) {
-        mService = Objects.requireNonNull(service);
+        mService = Objects.requireNonNull(service, "broadcast radio service cannot be null");
         mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock);
         mV1Modules = mHal1.loadModules();
         OptionalInt max = mV1Modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max();
@@ -61,6 +62,18 @@
                 max.isPresent() ? max.getAsInt() + 1 : 0, mLock);
     }
 
+    @VisibleForTesting
+    IRadioServiceHidlImpl(BroadcastRadioService service,
+            com.android.server.broadcastradio.hal1.BroadcastRadioService hal1,
+            com.android.server.broadcastradio.hal2.BroadcastRadioService hal2) {
+        mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
+        mHal1 = Objects.requireNonNull(hal1,
+                "Broadcast radio service implementation for HIDL 1 HAL cannot be null");
+        mV1Modules = mHal1.loadModules();
+        mHal2 = Objects.requireNonNull(hal2,
+                "Broadcast radio service implementation for HIDL 2 HAL cannot be null");
+    }
+
     @Override
     public List<RadioManager.ModuleProperties> listModules() {
         mService.enforcePolicyAccess();
@@ -95,8 +108,8 @@
         if (isDebugEnabled()) {
             Slog.d(TAG, "Adding announcement listener for " + Arrays.toString(enabledTypes));
         }
-        Objects.requireNonNull(enabledTypes);
-        Objects.requireNonNull(listener);
+        Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null");
+        Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 587db41..5eb15e0 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -40,6 +40,7 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.app.compat.CompatChanges;
@@ -101,6 +102,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.sysprop.DisplayProperties;
 import android.text.TextUtils;
@@ -202,8 +204,6 @@
     private static final String FORCE_WIFI_DISPLAY_ENABLE = "persist.debug.wfd.enable";
 
     private static final String PROP_DEFAULT_DISPLAY_TOP_INSET = "persist.sys.displayinset.top";
-    private static final String PROP_USE_NEW_DISPLAY_POWER_CONTROLLER =
-            "persist.sys.use_new_display_power_controller";
     private static final long WAIT_FOR_DEFAULT_DISPLAY_TIMEOUT = 10000;
     // This value needs to be in sync with the threshold
     // in RefreshRateConfigs::getFrameRateDivisor.
@@ -1356,11 +1356,19 @@
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mSyncRoot) {
-                final int displayId = createVirtualDisplayLocked(callback, projection, callingUid,
-                        packageName, surface, flags, virtualDisplayConfig);
+                final int displayId =
+                        createVirtualDisplayLocked(
+                                callback,
+                                projection,
+                                callingUid,
+                                packageName,
+                                virtualDevice,
+                                surface,
+                                flags,
+                                virtualDisplayConfig);
                 if (displayId != Display.INVALID_DISPLAY && virtualDevice != null && dwpc != null) {
-                    mDisplayWindowPolicyControllers.put(displayId,
-                            Pair.create(virtualDevice, dwpc));
+                    mDisplayWindowPolicyControllers.put(
+                            displayId, Pair.create(virtualDevice, dwpc));
                 }
                 return displayId;
             }
@@ -1369,12 +1377,20 @@
         }
     }
 
-    private int createVirtualDisplayLocked(IVirtualDisplayCallback callback,
-            IMediaProjection projection, int callingUid, String packageName, Surface surface,
-            int flags, VirtualDisplayConfig virtualDisplayConfig) {
+    private int createVirtualDisplayLocked(
+            IVirtualDisplayCallback callback,
+            IMediaProjection projection,
+            int callingUid,
+            String packageName,
+            IVirtualDevice virtualDevice,
+            Surface surface,
+            int flags,
+            VirtualDisplayConfig virtualDisplayConfig) {
         if (mVirtualDisplayAdapter == null) {
-            Slog.w(TAG, "Rejecting request to create private virtual display "
-                    + "because the virtual display adapter is not available.");
+            Slog.w(
+                    TAG,
+                    "Rejecting request to create private virtual display "
+                            + "because the virtual display adapter is not available.");
             return -1;
         }
 
@@ -1385,6 +1401,19 @@
             return -1;
         }
 
+        // If the display is to be added to a device display group, we need to make the
+        // LogicalDisplayMapper aware of the link between the new display and its associated virtual
+        // device before triggering DISPLAY_DEVICE_EVENT_ADDED.
+        if (virtualDevice != null && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0) {
+            try {
+                final int virtualDeviceId = virtualDevice.getDeviceId();
+                mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(
+                        device, virtualDeviceId);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+
         // DisplayDevice events are handled manually for Virtual Displays.
         // TODO: multi-display Fix this so that generic add/remove events are not handled in a
         // different code path for virtual displays.  Currently this happens so that we can
@@ -1393,8 +1422,7 @@
         // called on the DisplayThread (which we don't want to wait for?).
         // One option would be to actually wait here on the binder thread
         // to be notified when the virtual display is created (or failed).
-        mDisplayDeviceRepo.onDisplayDeviceEvent(device,
-                DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+        mDisplayDeviceRepo.onDisplayDeviceEvent(device, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
 
         final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
         if (display != null) {
@@ -2575,6 +2603,7 @@
         mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked);
     }
 
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     private void addDisplayPowerControllerLocked(LogicalDisplay display) {
         if (mPowerHandler == null) {
             // initPowerManagement has not yet been called.
@@ -2588,7 +2617,8 @@
                 display, mSyncRoot);
         final DisplayPowerControllerInterface displayPowerController;
 
-        if (SystemProperties.getInt(PROP_USE_NEW_DISPLAY_POWER_CONTROLLER, 0) == 1) {
+        if (DeviceConfig.getBoolean("display_manager",
+                "use_newly_structured_display_power_controller", false)) {
             displayPowerController = new DisplayPowerController2(
                     mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
                     mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 70c9e23..cb97e28 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -28,6 +28,7 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
@@ -123,6 +124,12 @@
     /** Map of all display groups indexed by display group id. */
     private final SparseArray<DisplayGroup> mDisplayGroups = new SparseArray<>();
 
+    /**
+     * Map of display groups which are linked to virtual devices (all displays in the group are
+     * linked to that device). Keyed by virtual device unique id.
+     */
+    private final SparseIntArray mDeviceDisplayGroupIds = new SparseIntArray();
+
     private final DisplayDeviceRepository mDisplayDeviceRepo;
     private final DeviceStateToLayoutMap mDeviceStateToLayoutMap;
     private final Listener mListener;
@@ -157,6 +164,12 @@
      */
     private final SparseIntArray mDisplayGroupsToUpdate = new SparseIntArray();
 
+    /**
+     * ArrayMap of display device unique ID to virtual device ID. Used in {@link
+     * #updateLogicalDisplaysLocked} to establish which Virtual Devices own which Virtual Displays.
+     */
+    private final ArrayMap<String, Integer> mVirtualDeviceDisplayMapping = new ArrayMap<>();
+
     private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
     private Layout mCurrentLayout = null;
     private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
@@ -362,6 +375,19 @@
         mDeviceStateToLayoutMap.dumpLocked(ipw);
     }
 
+    /**
+     * Creates an association between a displayDevice and a virtual device. Any displays associated
+     * with this virtual device will be grouped together in a single {@link DisplayGroup} unless
+     * created with {@link Display.FLAG_OWN_DISPLAY_GROUP}.
+     *
+     * @param displayDevice the displayDevice to be linked
+     * @param virtualDeviceUniqueId the unique ID of the virtual device.
+     */
+    void associateDisplayDeviceWithVirtualDevice(
+            DisplayDevice displayDevice, int virtualDeviceUniqueId) {
+        mVirtualDeviceDisplayMapping.put(displayDevice.getUniqueId(), virtualDeviceUniqueId);
+    }
+
     void setDeviceStateLocked(int state, boolean isOverrideActive) {
         Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState
                 + ", interactive=" + mInteractive);
@@ -556,6 +582,9 @@
         }
         DisplayDeviceInfo deviceInfo = device.getDisplayDeviceInfoLocked();
 
+        // Remove any virtual device mapping which exists for the display.
+        mVirtualDeviceDisplayMapping.remove(device.getUniqueId());
+
         if (layoutDisplay.getAddress().equals(deviceInfo.address)) {
             layout.removeDisplayLocked(DEFAULT_DISPLAY);
 
@@ -749,24 +778,44 @@
                 // We wait until we sent the EVENT_REMOVED event before actually removing the
                 // group.
                 mDisplayGroups.delete(id);
+                // Remove possible reference to the removed group.
+                int deviceIndex = mDeviceDisplayGroupIds.indexOfValue(id);
+                if (deviceIndex >= 0) {
+                    mDeviceDisplayGroupIds.removeAt(deviceIndex);
+                }
             }
         }
     }
 
     private void assignDisplayGroupLocked(LogicalDisplay display) {
         final int displayId = display.getDisplayIdLocked();
+        final String primaryDisplayUniqueId = display.getPrimaryDisplayDeviceLocked().getUniqueId();
+        final Integer linkedDeviceUniqueId =
+                mVirtualDeviceDisplayMapping.get(primaryDisplayUniqueId);
 
         // Get current display group data
         int groupId = getDisplayGroupIdFromDisplayIdLocked(displayId);
+        Integer deviceDisplayGroupId = null;
+        if (linkedDeviceUniqueId != null
+                && mDeviceDisplayGroupIds.indexOfKey(linkedDeviceUniqueId) > 0) {
+            deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId);
+        }
         final DisplayGroup oldGroup = getDisplayGroupLocked(groupId);
 
         // Get the new display group if a change is needed
         final DisplayInfo info = display.getDisplayInfoLocked();
         final boolean needsOwnDisplayGroup = (info.flags & Display.FLAG_OWN_DISPLAY_GROUP) != 0;
         final boolean hasOwnDisplayGroup = groupId != Display.DEFAULT_DISPLAY_GROUP;
+        final boolean needsDeviceDisplayGroup =
+                !needsOwnDisplayGroup && linkedDeviceUniqueId != null;
+        final boolean hasDeviceDisplayGroup =
+                deviceDisplayGroupId != null && groupId == deviceDisplayGroupId;
         if (groupId == Display.INVALID_DISPLAY_GROUP
-                || hasOwnDisplayGroup != needsOwnDisplayGroup) {
-            groupId = assignDisplayGroupIdLocked(needsOwnDisplayGroup);
+                || hasOwnDisplayGroup != needsOwnDisplayGroup
+                || hasDeviceDisplayGroup != needsDeviceDisplayGroup) {
+            groupId =
+                    assignDisplayGroupIdLocked(
+                            needsOwnDisplayGroup, needsDeviceDisplayGroup, linkedDeviceUniqueId);
         }
 
         // Create a new group if needed
@@ -931,7 +980,17 @@
         display.setPhase(phase);
     }
 
-    private int assignDisplayGroupIdLocked(boolean isOwnDisplayGroup) {
+    private int assignDisplayGroupIdLocked(
+            boolean isOwnDisplayGroup, boolean isDeviceDisplayGroup, Integer linkedDeviceUniqueId) {
+        if (isDeviceDisplayGroup && linkedDeviceUniqueId != null) {
+            int deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId);
+            // A value of 0 indicates that no device display group was found.
+            if (deviceDisplayGroupId == 0) {
+                deviceDisplayGroupId = mNextNonDefaultGroupId++;
+                mDeviceDisplayGroupIds.put(linkedDeviceUniqueId, deviceDisplayGroupId);
+            }
+            return deviceDisplayGroupId;
+        }
         return isOwnDisplayGroup ? mNextNonDefaultGroupId++ : Display.DEFAULT_DISPLAY_GROUP;
     }
 
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index b8af1bf..cd9ef09 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -16,6 +16,9 @@
 
 package com.android.server.dreams;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
+
+import android.app.ActivityTaskManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -34,13 +37,13 @@
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamService;
 import android.util.Slog;
-import android.view.IWindowManager;
-import android.view.WindowManagerGlobal;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.NoSuchElementException;
 
 /**
@@ -60,9 +63,7 @@
     private final Context mContext;
     private final Handler mHandler;
     private final Listener mListener;
-    private final IWindowManager mIWindowManager;
-    private long mDreamStartTime;
-    private String mSavedStopReason;
+    private final ActivityTaskManager mActivityTaskManager;
 
     private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
             .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
@@ -73,27 +74,21 @@
 
     private DreamRecord mCurrentDream;
 
-    private final Runnable mStopUnconnectedDreamRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) {
-                Slog.w(TAG, "Bound dream did not connect in the time allotted");
-                stopDream(true /*immediate*/, "slow to connect");
-            }
-        }
-    };
+    // Whether a dreaming started intent has been broadcast.
+    private boolean mSentStartBroadcast = false;
 
-    private final Runnable mStopStubbornDreamRunnable = () -> {
-        Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
-        stopDream(true /*immediate*/, "slow to finish");
-        mSavedStopReason = null;
-    };
+    // When a new dream is started and there is an existing dream, the existing dream is allowed to
+    // live a little longer until the new dream is started, for a smoother transition. This dream is
+    // stopped as soon as the new dream is started, and this list is cleared. Usually there should
+    // only be one previous dream while waiting for a new dream to start, but we store a list to
+    // proof the edge case of multiple previous dreams.
+    private final ArrayList<DreamRecord> mPreviousDreams = new ArrayList<>();
 
     public DreamController(Context context, Handler handler, Listener listener) {
         mContext = context;
         mHandler = handler;
         mListener = listener;
-        mIWindowManager = WindowManagerGlobal.getWindowManagerService();
+        mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
         mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         mCloseNotificationShadeIntent.putExtra("reason", "dream");
     }
@@ -109,18 +104,17 @@
             pw.println("    mUserId=" + mCurrentDream.mUserId);
             pw.println("    mBound=" + mCurrentDream.mBound);
             pw.println("    mService=" + mCurrentDream.mService);
-            pw.println("    mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast);
             pw.println("    mWakingGently=" + mCurrentDream.mWakingGently);
         } else {
             pw.println("  mCurrentDream: null");
         }
+
+        pw.println("  mSentStartBroadcast=" + mSentStartBroadcast);
     }
 
     public void startDream(Binder token, ComponentName name,
             boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock,
             ComponentName overlayComponentName, String reason) {
-        stopDream(true /*immediate*/, "starting new dream");
-
         Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream");
         try {
             // Close the notification shade. No need to send to all, but better to be explicit.
@@ -130,9 +124,12 @@
                     + ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze
                     + ", userId=" + userId + ", reason='" + reason + "'");
 
+            if (mCurrentDream != null) {
+                mPreviousDreams.add(mCurrentDream);
+            }
             mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock);
 
-            mDreamStartTime = SystemClock.elapsedRealtime();
+            mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime();
             MetricsLogger.visible(mContext,
                     mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
 
@@ -155,31 +152,49 @@
             }
 
             mCurrentDream.mBound = true;
-            mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT);
+            mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable,
+                    DREAM_CONNECTION_TIMEOUT);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
     }
 
+    /**
+     * Stops dreaming.
+     *
+     * The current dream, if any, and any unstopped previous dreams are stopped. The device stops
+     * dreaming.
+     */
     public void stopDream(boolean immediate, String reason) {
-        if (mCurrentDream == null) {
+        stopPreviousDreams();
+        stopDreamInstance(immediate, reason, mCurrentDream);
+    }
+
+    /**
+     * Stops the given dream instance.
+     *
+     * The device may still be dreaming afterwards if there are other dreams running.
+     */
+    private void stopDreamInstance(boolean immediate, String reason, DreamRecord dream) {
+        if (dream == null) {
             return;
         }
 
         Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream");
         try {
             if (!immediate) {
-                if (mCurrentDream.mWakingGently) {
+                if (dream.mWakingGently) {
                     return; // already waking gently
                 }
 
-                if (mCurrentDream.mService != null) {
+                if (dream.mService != null) {
                     // Give the dream a moment to wake up and finish itself gently.
-                    mCurrentDream.mWakingGently = true;
+                    dream.mWakingGently = true;
                     try {
-                        mSavedStopReason = reason;
-                        mCurrentDream.mService.wakeUp();
-                        mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT);
+                        dream.mStopReason = reason;
+                        dream.mService.wakeUp();
+                        mHandler.postDelayed(dream.mStopStubbornDreamRunnable,
+                                DREAM_FINISH_TIMEOUT);
                         return;
                     } catch (RemoteException ex) {
                         // oh well, we tried, finish immediately instead
@@ -187,54 +202,76 @@
                 }
             }
 
-            final DreamRecord oldDream = mCurrentDream;
-            mCurrentDream = null;
-            Slog.i(TAG, "Stopping dream: name=" + oldDream.mName
-                    + ", isPreviewMode=" + oldDream.mIsPreviewMode
-                    + ", canDoze=" + oldDream.mCanDoze
-                    + ", userId=" + oldDream.mUserId
+            Slog.i(TAG, "Stopping dream: name=" + dream.mName
+                    + ", isPreviewMode=" + dream.mIsPreviewMode
+                    + ", canDoze=" + dream.mCanDoze
+                    + ", userId=" + dream.mUserId
                     + ", reason='" + reason + "'"
-                    + (mSavedStopReason == null ? "" : "(from '" + mSavedStopReason + "')"));
+                    + (dream.mStopReason == null ? "" : "(from '"
+                    + dream.mStopReason + "')"));
             MetricsLogger.hidden(mContext,
-                    oldDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
+                    dream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
             MetricsLogger.histogram(mContext,
-                    oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" ,
-                    (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L)));
+                    dream.mCanDoze ? "dozing_minutes" : "dreaming_minutes",
+                    (int) ((SystemClock.elapsedRealtime() - dream.mDreamStartTime) / (1000L
+                            * 60L)));
 
-            mHandler.removeCallbacks(mStopUnconnectedDreamRunnable);
-            mHandler.removeCallbacks(mStopStubbornDreamRunnable);
-            mSavedStopReason = null;
+            mHandler.removeCallbacks(dream.mStopUnconnectedDreamRunnable);
+            mHandler.removeCallbacks(dream.mStopStubbornDreamRunnable);
 
-            if (oldDream.mSentStartBroadcast) {
-                mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
-            }
-
-            if (oldDream.mService != null) {
+            if (dream.mService != null) {
                 try {
-                    oldDream.mService.detach();
+                    dream.mService.detach();
                 } catch (RemoteException ex) {
                     // we don't care; this thing is on the way out
                 }
 
                 try {
-                    oldDream.mService.asBinder().unlinkToDeath(oldDream, 0);
+                    dream.mService.asBinder().unlinkToDeath(dream, 0);
                 } catch (NoSuchElementException ex) {
                     // don't care
                 }
-                oldDream.mService = null;
+                dream.mService = null;
             }
 
-            if (oldDream.mBound) {
-                mContext.unbindService(oldDream);
+            if (dream.mBound) {
+                mContext.unbindService(dream);
             }
-            oldDream.releaseWakeLockIfNeeded();
+            dream.releaseWakeLockIfNeeded();
 
-            mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken));
+            // Current dream stopped, device no longer dreaming.
+            if (dream == mCurrentDream) {
+                mCurrentDream = null;
+
+                if (mSentStartBroadcast) {
+                    mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
+                }
+
+                mActivityTaskManager.removeRootTasksWithActivityTypes(
+                        new int[] {ACTIVITY_TYPE_DREAM});
+
+                mListener.onDreamStopped(dream.mToken);
+            }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
     }
 
+    /**
+     * Stops all previous dreams, if any.
+     */
+    private void stopPreviousDreams() {
+        if (mPreviousDreams.isEmpty()) {
+            return;
+        }
+
+        // Using an iterator because mPreviousDreams is modified while the iteration is in process.
+        for (final Iterator<DreamRecord> it = mPreviousDreams.iterator(); it.hasNext(); ) {
+            stopDreamInstance(true /*immediate*/, "stop previous dream", it.next());
+            it.remove();
+        }
+    }
+
     private void attach(IDreamService service) {
         try {
             service.asBinder().linkToDeath(mCurrentDream, 0);
@@ -248,9 +285,9 @@
 
         mCurrentDream.mService = service;
 
-        if (!mCurrentDream.mIsPreviewMode) {
+        if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) {
             mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL);
-            mCurrentDream.mSentStartBroadcast = true;
+            mSentStartBroadcast = true;
         }
     }
 
@@ -272,10 +309,35 @@
         public boolean mBound;
         public boolean mConnected;
         public IDreamService mService;
-        public boolean mSentStartBroadcast;
-
+        private String mStopReason;
+        private long mDreamStartTime;
         public boolean mWakingGently;
 
+        private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded;
+        private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
+
+        private final Runnable mStopUnconnectedDreamRunnable = () -> {
+            if (mBound && !mConnected) {
+                Slog.w(TAG, "Bound dream did not connect in the time allotted");
+                stopDream(true /*immediate*/, "slow to connect" /*reason*/);
+            }
+        };
+
+        private final Runnable mStopStubbornDreamRunnable = () -> {
+            Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
+            stopDream(true /*immediate*/, "slow to finish" /*reason*/);
+            mStopReason = null;
+        };
+
+        private final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
+            // May be called on any thread.
+            @Override
+            public void sendResult(Bundle data) {
+                mHandler.post(mStopPreviousDreamsIfNeeded);
+                mHandler.post(mReleaseWakeLockIfNeeded);
+            }
+        };
+
         DreamRecord(Binder token, ComponentName name, boolean isPreviewMode,
                 boolean canDoze, int userId, PowerManager.WakeLock wakeLock) {
             mToken = token;
@@ -286,7 +348,9 @@
             mWakeLock = wakeLock;
             // Hold the lock while we're waiting for the service to connect and start dreaming.
             // Released after the service has started dreaming, we stop dreaming, or it timed out.
-            mWakeLock.acquire();
+            if (mWakeLock != null) {
+                mWakeLock.acquire();
+            }
             mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000);
         }
 
@@ -326,6 +390,12 @@
             });
         }
 
+        void stopPreviousDreamsIfNeeded() {
+            if (mCurrentDream == DreamRecord.this) {
+                stopPreviousDreams();
+            }
+        }
+
         void releaseWakeLockIfNeeded() {
             if (mWakeLock != null) {
                 mWakeLock.release();
@@ -333,15 +403,5 @@
                 mHandler.removeCallbacks(mReleaseWakeLockIfNeeded);
             }
         }
-
-        final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
-
-        final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
-            // May be called on any thread.
-            @Override
-            public void sendResult(Bundle data) throws RemoteException {
-                mHandler.post(mReleaseWakeLockIfNeeded);
-            }
-        };
     }
 }
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 951a8a2..6e2cceb 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -493,8 +493,6 @@
             return;
         }
 
-        stopDreamLocked(true /*immediate*/, "starting new dream");
-
         Slog.i(TAG, "Entering dreamland.");
 
         mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze);
diff --git a/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java b/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java
index 5253d34..d4e8f27 100644
--- a/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java
+++ b/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java
@@ -19,28 +19,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringRes;
-import android.annotation.UserIdInt;
-import android.app.AppGlobals;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ServiceInfo;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-import android.text.TextUtils;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.util.TimeUtils;
-
-import com.android.internal.annotations.GuardedBy;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 
 /**
  * Gets the service name using a framework resources, temporarily changing the service if necessary
@@ -48,259 +29,42 @@
  *
  * @hide
  */
-public final class FrameworkResourcesServiceNameResolver implements ServiceNameResolver {
+public final class FrameworkResourcesServiceNameResolver extends ServiceNameBaseResolver {
 
-    private static final String TAG = FrameworkResourcesServiceNameResolver.class.getSimpleName();
-
-    /** Handler message to {@link #resetTemporaryService(int)} */
-    private static final int MSG_RESET_TEMPORARY_SERVICE = 0;
-
-    @NonNull
-    private final Context mContext;
-    @NonNull
-    private final Object mLock = new Object();
-    @StringRes
     private final int mStringResourceId;
     @ArrayRes
     private final int mArrayResourceId;
-    private final boolean mIsMultiple;
-    /**
-     * Map of temporary service name list set by {@link #setTemporaryServices(int, String[], int)},
-     * keyed by {@code userId}.
-     *
-     * <p>Typically used by Shell command and/or CTS tests to configure temporary services if
-     * mIsMultiple is true.
-     */
-    @GuardedBy("mLock")
-    private final SparseArray<String[]> mTemporaryServiceNamesList = new SparseArray<>();
-    /**
-     * Map of default services that have been disabled by
-     * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}.
-     *
-     * <p>Typically used by Shell command and/or CTS tests.
-     */
-    @GuardedBy("mLock")
-    private final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray();
-    @Nullable
-    private NameResolverListener mOnSetCallback;
-    /**
-     * When the temporary service will expire (and reset back to the default).
-     */
-    @GuardedBy("mLock")
-    private long mTemporaryServiceExpiration;
-
-    /**
-     * Handler used to reset the temporary service name.
-     */
-    @GuardedBy("mLock")
-    private Handler mTemporaryHandler;
 
     public FrameworkResourcesServiceNameResolver(@NonNull Context context,
             @StringRes int resourceId) {
-        mContext = context;
+        super(context, false);
         mStringResourceId = resourceId;
         mArrayResourceId = -1;
-        mIsMultiple = false;
     }
 
     public FrameworkResourcesServiceNameResolver(@NonNull Context context,
             @ArrayRes int resourceId, boolean isMultiple) {
+        super(context, isMultiple);
         if (!isMultiple) {
             throw new UnsupportedOperationException("Please use "
                     + "FrameworkResourcesServiceNameResolver(context, @StringRes int) constructor "
                     + "if single service mode is requested.");
         }
-        mContext = context;
         mStringResourceId = -1;
         mArrayResourceId = resourceId;
-        mIsMultiple = true;
     }
 
     @Override
-    public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) {
-        synchronized (mLock) {
-            this.mOnSetCallback = callback;
-        }
+    public String[] readServiceNameList(int userId) {
+        return mContext.getResources().getStringArray(mArrayResourceId);
     }
 
+    @Nullable
     @Override
-    public String getServiceName(@UserIdInt int userId) {
-        String[] serviceNames = getServiceNameList(userId);
-        return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0];
+    public String readServiceName(int userId) {
+        return mContext.getResources().getString(mStringResourceId);
     }
 
-    @Override
-    public String getDefaultServiceName(@UserIdInt int userId) {
-        String[] serviceNames = getDefaultServiceNameList(userId);
-        return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0];
-    }
-
-    /**
-     * Gets the default list of the service names for the given user.
-     *
-     * <p>Typically implemented by services which want to provide multiple backends.
-     */
-    @Override
-    public String[] getServiceNameList(int userId) {
-        synchronized (mLock) {
-            String[] temporaryNames = mTemporaryServiceNamesList.get(userId);
-            if (temporaryNames != null) {
-                // Always log it, as it should only be used on CTS or during development
-                Slog.w(TAG, "getServiceName(): using temporary name "
-                        + Arrays.toString(temporaryNames) + " for user " + userId);
-                return temporaryNames;
-            }
-            final boolean disabled = mDefaultServicesDisabled.get(userId);
-            if (disabled) {
-                // Always log it, as it should only be used on CTS or during development
-                Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for "
-                        + "user " + userId);
-                return null;
-            }
-            return getDefaultServiceNameList(userId);
-
-        }
-    }
-
-    /**
-     * Gets the default list of the service names for the given user.
-     *
-     * <p>Typically implemented by services which want to provide multiple backends.
-     */
-    @Override
-    public String[] getDefaultServiceNameList(int userId) {
-        synchronized (mLock) {
-            if (mIsMultiple) {
-                String[] serviceNameList = mContext.getResources().getStringArray(mArrayResourceId);
-                // Filter out unimplemented services
-                // Initialize the validated array as null because we do not know the final size.
-                List<String> validatedServiceNameList = new ArrayList<>();
-                try {
-                    for (int i = 0; i < serviceNameList.length; i++) {
-                        if (TextUtils.isEmpty(serviceNameList[i])) {
-                            continue;
-                        }
-                        ComponentName serviceComponent = ComponentName.unflattenFromString(
-                                serviceNameList[i]);
-                        ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
-                                serviceComponent,
-                                PackageManager.MATCH_DIRECT_BOOT_AWARE
-                                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
-                        if (serviceInfo != null) {
-                            validatedServiceNameList.add(serviceNameList[i]);
-                        }
-                    }
-                } catch (Exception e) {
-                    Slog.e(TAG, "Could not validate provided services.", e);
-                }
-                String[] validatedServiceNameArray = new String[validatedServiceNameList.size()];
-                return validatedServiceNameList.toArray(validatedServiceNameArray);
-            } else {
-                final String name = mContext.getString(mStringResourceId);
-                return TextUtils.isEmpty(name) ? new String[0] : new String[]{name};
-            }
-        }
-    }
-
-    @Override
-    public boolean isConfiguredInMultipleMode() {
-        return mIsMultiple;
-    }
-
-    @Override
-    public boolean isTemporary(@UserIdInt int userId) {
-        synchronized (mLock) {
-            return mTemporaryServiceNamesList.get(userId) != null;
-        }
-    }
-
-    @Override
-    public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName,
-            int durationMs) {
-        setTemporaryServices(userId, new String[]{componentName}, durationMs);
-    }
-
-    @Override
-    public void setTemporaryServices(int userId, @NonNull String[] componentNames, int durationMs) {
-        synchronized (mLock) {
-            mTemporaryServiceNamesList.put(userId, componentNames);
-
-            if (mTemporaryHandler == null) {
-                mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
-                    @Override
-                    public void handleMessage(Message msg) {
-                        if (msg.what == MSG_RESET_TEMPORARY_SERVICE) {
-                            synchronized (mLock) {
-                                resetTemporaryService(userId);
-                            }
-                        } else {
-                            Slog.wtf(TAG, "invalid handler msg: " + msg);
-                        }
-                    }
-                };
-            } else {
-                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
-            }
-            mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs;
-            mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs);
-            for (int i = 0; i < componentNames.length; i++) {
-                notifyTemporaryServiceNameChangedLocked(userId, componentNames[i],
-                        /* isTemporary= */ true);
-            }
-        }
-    }
-
-    @Override
-    public void resetTemporaryService(@UserIdInt int userId) {
-        synchronized (mLock) {
-            Slog.i(TAG, "resetting temporary service for user " + userId + " from "
-                    + Arrays.toString(mTemporaryServiceNamesList.get(userId)));
-            mTemporaryServiceNamesList.remove(userId);
-            if (mTemporaryHandler != null) {
-                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
-                mTemporaryHandler = null;
-            }
-            notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null,
-                    /* isTemporary= */ false);
-        }
-    }
-
-    @Override
-    public boolean setDefaultServiceEnabled(int userId, boolean enabled) {
-        synchronized (mLock) {
-            final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId);
-            if (currentlyEnabled == enabled) {
-                Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled);
-                return false;
-            }
-            if (enabled) {
-                Slog.i(TAG, "disabling default service for user " + userId);
-                mDefaultServicesDisabled.removeAt(userId);
-            } else {
-                Slog.i(TAG, "enabling default service for user " + userId);
-                mDefaultServicesDisabled.put(userId, true);
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public boolean isDefaultServiceEnabled(int userId) {
-        synchronized (mLock) {
-            return isDefaultServiceEnabledLocked(userId);
-        }
-    }
-
-    private boolean isDefaultServiceEnabledLocked(int userId) {
-        return !mDefaultServicesDisabled.get(userId);
-    }
-
-    @Override
-    public String toString() {
-        synchronized (mLock) {
-            return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNamesList + "]";
-        }
-    }
 
     // TODO(b/117779333): support proto
     @Override
@@ -314,31 +78,4 @@
             pw.print(mDefaultServicesDisabled.size());
         }
     }
-
-    // TODO(b/117779333): support proto
-    @Override
-    public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) {
-        synchronized (mLock) {
-            final String[] temporaryNames = mTemporaryServiceNamesList.get(userId);
-            if (temporaryNames != null) {
-                pw.print("tmpName=");
-                pw.print(Arrays.toString(temporaryNames));
-                final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime();
-                pw.print(" (expires in ");
-                TimeUtils.formatDuration(ttl, pw);
-                pw.print("), ");
-            }
-            pw.print("defaultName=");
-            pw.print(getDefaultServiceName(userId));
-            final boolean disabled = mDefaultServicesDisabled.get(userId);
-            pw.println(disabled ? " (disabled)" : " (enabled)");
-        }
-    }
-
-    private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId,
-            @Nullable String newTemporaryName, boolean isTemporary) {
-        if (mOnSetCallback != null) {
-            mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary);
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java b/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java
index cac7f53..17d75e6 100644
--- a/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java
+++ b/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java
@@ -19,8 +19,11 @@
 import android.annotation.UserIdInt;
 import android.content.Context;
 import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArraySet;
 
 import java.io.PrintWriter;
+import java.util.Set;
 
 /**
  * Gets the service name using a property from the {@link android.provider.Settings.Secure}
@@ -28,21 +31,34 @@
  *
  * @hide
  */
-public final class SecureSettingsServiceNameResolver implements ServiceNameResolver {
+public final class SecureSettingsServiceNameResolver extends ServiceNameBaseResolver {
+    /**
+     * The delimiter to be used to parse the secure settings string. Services must make sure
+     * that this delimiter is used while adding component names to their secure setting property.
+     */
+    private static final char COMPONENT_NAME_SEPARATOR = ':';
 
-    private final @NonNull Context mContext;
+    private final TextUtils.SimpleStringSplitter mStringColonSplitter =
+            new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR);
 
     @NonNull
     private final String mProperty;
 
     public SecureSettingsServiceNameResolver(@NonNull Context context, @NonNull String property) {
-        mContext = context;
-        mProperty = property;
+        this(context, property, /*isMultiple*/false);
     }
 
-    @Override
-    public String getDefaultServiceName(@UserIdInt int userId) {
-        return Settings.Secure.getStringForUser(mContext.getContentResolver(), mProperty, userId);
+    /**
+     *
+     * @param context the context required to retrieve the secure setting value
+     * @param property name of the secure setting key
+     * @param isMultiple true if the system service using this resolver needs to connect to
+     *                   multiple remote services, false otherwise
+     */
+    public SecureSettingsServiceNameResolver(@NonNull Context context, @NonNull String property,
+            boolean isMultiple) {
+        super(context, isMultiple);
+        mProperty = property;
     }
 
     // TODO(b/117779333): support proto
@@ -61,4 +77,34 @@
     public String toString() {
         return "SecureSettingsServiceNameResolver[" + mProperty + "]";
     }
+
+    @Override
+    public String[] readServiceNameList(int userId) {
+        return parseColonDelimitedServiceNames(
+                Settings.Secure.getStringForUser(
+                        mContext.getContentResolver(), mProperty, userId));
+    }
+
+    @Override
+    public String readServiceName(int userId) {
+        return Settings.Secure.getStringForUser(
+                mContext.getContentResolver(), mProperty, userId);
+    }
+
+    private String[] parseColonDelimitedServiceNames(String serviceNames) {
+        final Set<String> delimitedServices = new ArraySet<>();
+        if (!TextUtils.isEmpty(serviceNames)) {
+            final TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
+            splitter.setString(serviceNames);
+            while (splitter.hasNext()) {
+                final String str = splitter.next();
+                if (TextUtils.isEmpty(str)) {
+                    continue;
+                }
+                delimitedServices.add(str);
+            }
+        }
+        String[] delimitedServicesArray = new String[delimitedServices.size()];
+        return delimitedServices.toArray(delimitedServicesArray);
+    }
 }
diff --git a/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java b/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java
new file mode 100644
index 0000000..76ea05e
--- /dev/null
+++ b/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2018 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.infra;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Gets the service name using a framework resources, temporarily changing the service if necessary
+ * (typically during CTS tests or service development).
+ *
+ * @hide
+ */
+public abstract class ServiceNameBaseResolver implements ServiceNameResolver {
+
+    private static final String TAG = ServiceNameBaseResolver.class.getSimpleName();
+
+    /** Handler message to {@link #resetTemporaryService(int)} */
+    private static final int MSG_RESET_TEMPORARY_SERVICE = 0;
+
+    @NonNull
+    protected final Context mContext;
+    @NonNull
+    protected final Object mLock = new Object();
+
+    protected final boolean mIsMultiple;
+    /**
+     * Map of temporary service name list set by {@link #setTemporaryServices(int, String[], int)},
+     * keyed by {@code userId}.
+     *
+     * <p>Typically used by Shell command and/or CTS tests to configure temporary services if
+     * mIsMultiple is true.
+     */
+    @GuardedBy("mLock")
+    protected final SparseArray<String[]> mTemporaryServiceNamesList = new SparseArray<>();
+    /**
+     * Map of default services that have been disabled by
+     * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}.
+     *
+     * <p>Typically used by Shell command and/or CTS tests.
+     */
+    @GuardedBy("mLock")
+    protected final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray();
+    @Nullable
+    private NameResolverListener mOnSetCallback;
+    /**
+     * When the temporary service will expire (and reset back to the default).
+     */
+    @GuardedBy("mLock")
+    private long mTemporaryServiceExpiration;
+
+    /**
+     * Handler used to reset the temporary service name.
+     */
+    @GuardedBy("mLock")
+    private Handler mTemporaryHandler;
+
+    protected ServiceNameBaseResolver(Context context, boolean isMultiple) {
+        mContext = context;
+        mIsMultiple = isMultiple;
+    }
+
+    @Override
+    public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) {
+        synchronized (mLock) {
+            this.mOnSetCallback = callback;
+        }
+    }
+
+    @Override
+    public String getServiceName(@UserIdInt int userId) {
+        String[] serviceNames = getServiceNameList(userId);
+        return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0];
+    }
+
+    @Override
+    public String getDefaultServiceName(@UserIdInt int userId) {
+        String[] serviceNames = getDefaultServiceNameList(userId);
+        return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0];
+    }
+
+    /**
+     * Gets the default list of the service names for the given user.
+     *
+     * <p>Typically implemented by services which want to provide multiple backends.
+     */
+    @Override
+    public String[] getServiceNameList(int userId) {
+        synchronized (mLock) {
+            String[] temporaryNames = mTemporaryServiceNamesList.get(userId);
+            if (temporaryNames != null) {
+                // Always log it, as it should only be used on CTS or during development
+                Slog.w(TAG, "getServiceName(): using temporary name "
+                        + Arrays.toString(temporaryNames) + " for user " + userId);
+                return temporaryNames;
+            }
+            final boolean disabled = mDefaultServicesDisabled.get(userId);
+            if (disabled) {
+                // Always log it, as it should only be used on CTS or during development
+                Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for "
+                        + "user " + userId);
+                return null;
+            }
+            return getDefaultServiceNameList(userId);
+
+        }
+    }
+
+    /**
+     * Base classes must override this to read from the desired config e.g. framework resource,
+     * secure settings etc.
+     */
+    @Nullable
+    public abstract String[] readServiceNameList(int userId);
+
+    /**
+     * Base classes must override this to read from the desired config e.g. framework resource,
+     * secure settings etc.
+     */
+    @Nullable
+    public abstract String readServiceName(int userId);
+
+    /**
+     * Gets the default list of the service names for the given user.
+     *
+     * <p>Typically implemented by services which want to provide multiple backends.
+     */
+    @Override
+    public String[] getDefaultServiceNameList(int userId) {
+        synchronized (mLock) {
+            if (mIsMultiple) {
+                String[] serviceNameList = readServiceNameList(userId);
+                // Filter out unimplemented services
+                // Initialize the validated array as null because we do not know the final size.
+                List<String> validatedServiceNameList = new ArrayList<>();
+                try {
+                    for (int i = 0; i < serviceNameList.length; i++) {
+                        if (TextUtils.isEmpty(serviceNameList[i])) {
+                            continue;
+                        }
+                        ComponentName serviceComponent = ComponentName.unflattenFromString(
+                                serviceNameList[i]);
+                        ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
+                                serviceComponent,
+                                PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
+                        if (serviceInfo != null) {
+                            validatedServiceNameList.add(serviceNameList[i]);
+                        }
+                    }
+                } catch (Exception e) {
+                    Slog.e(TAG, "Could not validate provided services.", e);
+                }
+                String[] validatedServiceNameArray = new String[validatedServiceNameList.size()];
+                return validatedServiceNameList.toArray(validatedServiceNameArray);
+            } else {
+                final String name = readServiceName(userId);
+                return TextUtils.isEmpty(name) ? new String[0] : new String[]{name};
+            }
+        }
+    }
+
+    @Override
+    public boolean isConfiguredInMultipleMode() {
+        return mIsMultiple;
+    }
+
+    @Override
+    public boolean isTemporary(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return mTemporaryServiceNamesList.get(userId) != null;
+        }
+    }
+
+    @Override
+    public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName,
+            int durationMs) {
+        setTemporaryServices(userId, new String[]{componentName}, durationMs);
+    }
+
+    @Override
+    public void setTemporaryServices(int userId, @NonNull String[] componentNames, int durationMs) {
+        synchronized (mLock) {
+            mTemporaryServiceNamesList.put(userId, componentNames);
+
+            if (mTemporaryHandler == null) {
+                mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
+                    @Override
+                    public void handleMessage(Message msg) {
+                        if (msg.what == MSG_RESET_TEMPORARY_SERVICE) {
+                            synchronized (mLock) {
+                                resetTemporaryService(userId);
+                            }
+                        } else {
+                            Slog.wtf(TAG, "invalid handler msg: " + msg);
+                        }
+                    }
+                };
+            } else {
+                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
+            }
+            mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs;
+            mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs);
+            for (int i = 0; i < componentNames.length; i++) {
+                notifyTemporaryServiceNameChangedLocked(userId, componentNames[i],
+                        /* isTemporary= */ true);
+            }
+        }
+    }
+
+    @Override
+    public void resetTemporaryService(@UserIdInt int userId) {
+        synchronized (mLock) {
+            Slog.i(TAG, "resetting temporary service for user " + userId + " from "
+                    + Arrays.toString(mTemporaryServiceNamesList.get(userId)));
+            mTemporaryServiceNamesList.remove(userId);
+            if (mTemporaryHandler != null) {
+                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
+                mTemporaryHandler = null;
+            }
+            notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null,
+                    /* isTemporary= */ false);
+        }
+    }
+
+    @Override
+    public boolean setDefaultServiceEnabled(int userId, boolean enabled) {
+        synchronized (mLock) {
+            final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId);
+            if (currentlyEnabled == enabled) {
+                Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled);
+                return false;
+            }
+            if (enabled) {
+                Slog.i(TAG, "disabling default service for user " + userId);
+                mDefaultServicesDisabled.removeAt(userId);
+            } else {
+                Slog.i(TAG, "enabling default service for user " + userId);
+                mDefaultServicesDisabled.put(userId, true);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean isDefaultServiceEnabled(int userId) {
+        synchronized (mLock) {
+            return isDefaultServiceEnabledLocked(userId);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean isDefaultServiceEnabledLocked(int userId) {
+        return !mDefaultServicesDisabled.get(userId);
+    }
+
+    @Override
+    public String toString() {
+        synchronized (mLock) {
+            return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNamesList + "]";
+        }
+    }
+
+    // TODO(b/117779333): support proto
+    @Override
+    public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) {
+        synchronized (mLock) {
+            final String[] temporaryNames = mTemporaryServiceNamesList.get(userId);
+            if (temporaryNames != null) {
+                pw.print("tmpName=");
+                pw.print(Arrays.toString(temporaryNames));
+                final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime();
+                pw.print(" (expires in ");
+                TimeUtils.formatDuration(ttl, pw);
+                pw.print("), ");
+            }
+            pw.print("defaultName=");
+            pw.print(getDefaultServiceName(userId));
+            final boolean disabled = mDefaultServicesDisabled.get(userId);
+            pw.println(disabled ? " (disabled)" : " (enabled)");
+        }
+    }
+
+    private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId,
+            @Nullable String newTemporaryName, boolean isTemporary) {
+        if (mOnSetCallback != null) {
+            mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java
index 324eefc..06ee7c4d 100644
--- a/services/core/java/com/android/server/input/BatteryController.java
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -44,6 +44,8 @@
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * A thread-safe component of {@link InputManagerService} responsible for managing the battery state
@@ -98,8 +100,12 @@
     }
 
     public void systemRunning() {
-        Objects.requireNonNull(mContext.getSystemService(InputManager.class))
-                .registerInputDeviceListener(mInputDeviceListener, mHandler);
+        final InputManager inputManager =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+        inputManager.registerInputDeviceListener(mInputDeviceListener, mHandler);
+        for (int deviceId : inputManager.getInputDeviceIds()) {
+            mInputDeviceListener.onInputDeviceAdded(deviceId);
+        }
     }
 
     /**
@@ -165,19 +171,20 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private void notifyAllListenersForDeviceLocked(State state) {
-        if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state);
-        mListenerRecords.forEach((pid, listenerRecord) -> {
-            if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) {
-                notifyBatteryListener(listenerRecord, state);
-            }
-        });
+    private void notifyAllListenersForDevice(State state) {
+        synchronized (mLock) {
+            if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state);
+            mListenerRecords.forEach((pid, listenerRecord) -> {
+                if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) {
+                    notifyBatteryListener(listenerRecord, state);
+                }
+            });
+        }
     }
 
     @GuardedBy("mLock")
     private void updatePollingLocked(boolean delayStart) {
-        if (mDeviceMonitors.isEmpty() || !mIsInteractive) {
+        if (!mIsInteractive || !anyOf(mDeviceMonitors, DeviceMonitor::requiresPolling)) {
             // Stop polling.
             mIsPolling = false;
             mHandler.removeCallbacks(this::handlePollEvent);
@@ -192,6 +199,13 @@
         mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0);
     }
 
+    private String getInputDeviceName(int deviceId) {
+        final InputDevice device =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
+                        .getInputDevice(deviceId);
+        return device != null ? device.getName() : "<none>";
+    }
+
     private boolean hasBattery(int deviceId) {
         final InputDevice device =
                 Objects.requireNonNull(mContext.getSystemService(InputManager.class))
@@ -199,6 +213,13 @@
         return device != null && device.hasBattery();
     }
 
+    private boolean isUsiDevice(int deviceId) {
+        final InputDevice device =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
+                        .getInputDevice(deviceId);
+        return device != null && device.supportsUsi();
+    }
+
     @GuardedBy("mLock")
     private DeviceMonitor getDeviceMonitorOrThrowLocked(int deviceId) {
         return Objects.requireNonNull(mDeviceMonitors.get(deviceId),
@@ -252,8 +273,10 @@
         if (!hasRegisteredListenerForDeviceLocked(deviceId)) {
             // There are no more listeners monitoring this device.
             final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId);
-            monitor.stopMonitoring();
-            mDeviceMonitors.remove(deviceId);
+            if (!monitor.isPersistent()) {
+                monitor.onMonitorDestroy();
+                mDeviceMonitors.remove(deviceId);
+            }
         }
 
         if (listenerRecord.mMonitoredDevices.isEmpty()) {
@@ -298,9 +321,7 @@
             if (monitor == null) {
                 return;
             }
-            if (monitor.updateBatteryState(eventTime)) {
-                notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-            }
+            monitor.onUEvent(eventTime);
         }
     }
 
@@ -310,14 +331,7 @@
                 return;
             }
             final long eventTime = SystemClock.uptimeMillis();
-            mDeviceMonitors.forEach((deviceId, monitor) -> {
-                // Re-acquire lock in the lambda to silence error-prone build warnings.
-                synchronized (mLock) {
-                    if (monitor.updateBatteryState(eventTime)) {
-                        notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-                    }
-                }
-            });
+            mDeviceMonitors.forEach((deviceId, monitor) -> monitor.onPoll(eventTime));
             mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS);
         }
     }
@@ -329,15 +343,11 @@
             final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
             if (monitor == null) {
                 // The input device's battery is not being monitored by any listener.
-                return queryBatteryStateFromNative(deviceId, updateTime);
+                return queryBatteryStateFromNative(deviceId, updateTime, hasBattery(deviceId));
             }
             // Force the battery state to update, and notify listeners if necessary.
-            final boolean stateChanged = monitor.updateBatteryState(updateTime);
-            final State state = monitor.getBatteryStateForReporting();
-            if (stateChanged) {
-                notifyAllListenersForDeviceLocked(state);
-            }
-            return state;
+            monitor.onPoll(updateTime);
+            return monitor.getBatteryStateForReporting();
         }
     }
 
@@ -379,7 +389,14 @@
     private final InputManager.InputDeviceListener mInputDeviceListener =
             new InputManager.InputDeviceListener() {
         @Override
-        public void onInputDeviceAdded(int deviceId) {}
+        public void onInputDeviceAdded(int deviceId) {
+            synchronized (mLock) {
+                if (isUsiDevice(deviceId) && !mDeviceMonitors.containsKey(deviceId)) {
+                    // Start monitoring USI device immediately.
+                    mDeviceMonitors.put(deviceId, new UsiDeviceMonitor(deviceId));
+                }
+            }
+        }
 
         @Override
         public void onInputDeviceRemoved(int deviceId) {}
@@ -392,9 +409,7 @@
                     return;
                 }
                 final long eventTime = SystemClock.uptimeMillis();
-                if (monitor.updateBatteryState(eventTime)) {
-                    notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-                }
+                monitor.onConfiguration(eventTime);
             }
         }
     };
@@ -422,8 +437,7 @@
     }
 
     // Queries the battery state of an input device from native code.
-    private State queryBatteryStateFromNative(int deviceId, long updateTime) {
-        final boolean isPresent = hasBattery(deviceId);
+    private State queryBatteryStateFromNative(int deviceId, long updateTime, boolean isPresent) {
         return new State(
                 deviceId,
                 updateTime,
@@ -434,8 +448,9 @@
 
     // Holds the state of an InputDevice for which battery changes are currently being monitored.
     private class DeviceMonitor {
-        @NonNull
-        private State mState;
+        private final State mState;
+        // Represents whether the input device has a sysfs battery node.
+        protected boolean mHasBattery = false;
 
         @Nullable
         private UEventBatteryListener mUEventBatteryListener;
@@ -445,26 +460,32 @@
 
             // Load the initial battery state and start monitoring.
             final long eventTime = SystemClock.uptimeMillis();
-            updateBatteryState(eventTime);
+            configureDeviceMonitor(eventTime);
         }
 
-        // Returns true if the battery state changed since the last time it was updated.
-        public boolean updateBatteryState(long updateTime) {
-            mState.updateTime = updateTime;
-
-            final State updatedState = queryBatteryStateFromNative(mState.deviceId, updateTime);
-            if (mState.equals(updatedState)) {
-                return false;
+        private void processChangesAndNotify(long eventTime, Consumer<Long> changes) {
+            final State oldState = getBatteryStateForReporting();
+            changes.accept(eventTime);
+            final State newState = getBatteryStateForReporting();
+            if (!oldState.equals(newState)) {
+                notifyAllListenersForDevice(newState);
             }
-            if (mState.isPresent != updatedState.isPresent) {
-                if (updatedState.isPresent) {
+        }
+
+        public void onConfiguration(long eventTime) {
+            processChangesAndNotify(eventTime, this::configureDeviceMonitor);
+        }
+
+        private void configureDeviceMonitor(long eventTime) {
+            if (mHasBattery != hasBattery(mState.deviceId)) {
+                mHasBattery = !mHasBattery;
+                if (mHasBattery) {
                     startMonitoring();
                 } else {
                     stopMonitoring();
                 }
+                updateBatteryStateFromNative(eventTime);
             }
-            mState = updatedState;
-            return true;
         }
 
         private void startMonitoring() {
@@ -483,19 +504,44 @@
                     mUEventBatteryListener, "DEVPATH=" + formatDevPath(batteryPath));
         }
 
-        private String formatDevPath(String path) {
+        private String formatDevPath(@NonNull String path) {
             // Remove the "/sys" prefix if it has one.
             return path.startsWith("/sys") ? path.substring(4) : path;
         }
 
-        // This must be called when the device is no longer being monitored.
-        public void stopMonitoring() {
+        private void stopMonitoring() {
             if (mUEventBatteryListener != null) {
                 mUEventManager.removeListener(mUEventBatteryListener);
                 mUEventBatteryListener = null;
             }
         }
 
+        // This must be called when the device is no longer being monitored.
+        public void onMonitorDestroy() {
+            stopMonitoring();
+        }
+
+        private void updateBatteryStateFromNative(long eventTime) {
+            mState.updateIfChanged(
+                    queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery));
+        }
+
+        public void onPoll(long eventTime) {
+            processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
+        }
+
+        public void onUEvent(long eventTime) {
+            processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
+        }
+
+        public boolean requiresPolling() {
+            return true;
+        }
+
+        public boolean isPersistent() {
+            return false;
+        }
+
         // Returns the current battery state that can be used to notify listeners BatteryController.
         public State getBatteryStateForReporting() {
             return new State(mState);
@@ -503,8 +549,31 @@
 
         @Override
         public String toString() {
-            return "state=" + mState
-                    + ", uEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
+            return "DeviceId=" + mState.deviceId
+                    + ", Name='" + getInputDeviceName(mState.deviceId) + "'"
+                    + ", NativeBattery=" + mState
+                    + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
+        }
+    }
+
+    // Battery monitoring logic that is specific to stylus devices that support the
+    // Universal Stylus Initiative (USI) protocol.
+    private class UsiDeviceMonitor extends DeviceMonitor {
+
+        UsiDeviceMonitor(int deviceId) {
+            super(deviceId);
+        }
+
+        @Override
+        public boolean requiresPolling() {
+            // Do not poll the battery state for USI devices.
+            return false;
+        }
+
+        @Override
+        public boolean isPersistent() {
+            // Do not remove the battery monitor for USI devices.
+            return true;
         }
     }
 
@@ -548,18 +617,33 @@
     private static class State extends IInputDeviceBatteryState {
 
         State(int deviceId) {
-            initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
-                    Float.NaN /*capacity*/);
+            reset(deviceId);
         }
 
         State(IInputDeviceBatteryState s) {
-            initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity);
+            copyFrom(s);
         }
 
         State(int deviceId, long updateTime, boolean isPresent, int status, float capacity) {
             initialize(deviceId, updateTime, isPresent, status, capacity);
         }
 
+        // Updates this from other if there is a difference between them, ignoring the updateTime.
+        public void updateIfChanged(IInputDeviceBatteryState other) {
+            if (!equalsIgnoringUpdateTime(other)) {
+                copyFrom(other);
+            }
+        }
+
+        private void reset(int deviceId) {
+            initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
+                    Float.NaN /*capacity*/);
+        }
+
+        private void copyFrom(IInputDeviceBatteryState s) {
+            initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity);
+        }
+
         private void initialize(int deviceId, long updateTime, boolean isPresent, int status,
                 float capacity) {
             this.deviceId = deviceId;
@@ -569,11 +653,34 @@
             this.capacity = capacity;
         }
 
+        private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) {
+            long updateTime = this.updateTime;
+            this.updateTime = other.updateTime;
+            boolean eq = this.equals(other);
+            this.updateTime = updateTime;
+            return eq;
+        }
+
         @Override
         public String toString() {
-            return "BatteryState{deviceId=" + deviceId + ", updateTime=" + updateTime
-                    + ", isPresent=" + isPresent + ", status=" + status + ", capacity=" + capacity
-                    + " }";
+            if (!isPresent) {
+                return "State{<not present>}";
+            }
+            return "State{time=" + updateTime
+                    + ", isPresent=" + isPresent
+                    + ", status=" + status
+                    + ", capacity=" + capacity
+                    + "}";
         }
     }
+
+    // Check if any value in an ArrayMap matches the predicate in an optimized way.
+    private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) {
+        for (int i = 0; i < arrayMap.size(); i++) {
+            if (test.test(arrayMap.valueAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 76331fd..76495b1 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -58,7 +58,6 @@
 import android.accessibilityservice.AccessibilityService;
 import android.annotation.AnyThread;
 import android.annotation.BinderThread;
-import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.DurationMillisLong;
 import android.annotation.EnforcePermission;
@@ -69,9 +68,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentProvider;
@@ -94,7 +90,6 @@
 import android.media.AudioManagerInternal;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.Debug;
 import android.os.Handler;
 import android.os.IBinder;
@@ -170,8 +165,6 @@
 import com.android.internal.inputmethod.StartInputFlags;
 import com.android.internal.inputmethod.StartInputReason;
 import com.android.internal.inputmethod.UnbindReason;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.TransferPipe;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.ConcurrentUtils;
@@ -255,13 +248,6 @@
     private static final String HANDLER_THREAD_NAME = "android.imms";
 
     /**
-     * A protected broadcast intent action for internal use for {@link PendingIntent} in
-     * the notification.
-     */
-    private static final String ACTION_SHOW_INPUT_METHOD_PICKER =
-            "com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER";
-
-    /**
      * When set, {@link #startInputUncheckedLocked} will return
      * {@link InputBindResult#NO_EDITOR} instead of starting an IME connection
      * unless {@link StartInputFlags#IS_TEXT_EDITOR} is set. This behavior overrides
@@ -334,13 +320,8 @@
     @GuardedBy("ImfLock.class")
     private int mDisplayIdToShowIme = INVALID_DISPLAY;
 
-    // Ongoing notification
-    private NotificationManager mNotificationManager;
     @Nullable private StatusBarManagerInternal mStatusBarManagerInternal;
-    private final Notification.Builder mImeSwitcherNotification;
-    private final PendingIntent mImeSwitchPendingIntent;
     private boolean mShowOngoingImeSwitcherForPhones;
-    private boolean mNotificationShown;
     @GuardedBy("ImfLock.class")
     private final HandwritingModeController mHwController;
     @GuardedBy("ImfLock.class")
@@ -1253,17 +1234,6 @@
                 return;
             } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
                 onActionLocaleChanged();
-            } else if (ACTION_SHOW_INPUT_METHOD_PICKER.equals(action)) {
-                // ACTION_SHOW_INPUT_METHOD_PICKER action is a protected-broadcast and it is
-                // guaranteed to be send only from the system, so that there is no need for extra
-                // security check such as
-                // {@link #canShowInputMethodPickerLocked(IInputMethodClient)}.
-                mHandler.obtainMessage(
-                        MSG_SHOW_IM_SUBTYPE_PICKER,
-                        // TODO(b/120076400): Design and implement IME switcher for heterogeneous
-                        // navbar configuration.
-                        InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES,
-                        DEFAULT_DISPLAY).sendToTarget();
             } else {
                 Slog.w(TAG, "Unexpected intent " + intent);
             }
@@ -1720,27 +1690,8 @@
 
         mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);
 
-        Bundle extras = new Bundle();
-        extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);
-        @ColorInt final int accentColor = mContext.getColor(
-                com.android.internal.R.color.system_notification_accent_color);
-        mImeSwitcherNotification =
-                new Notification.Builder(mContext, SystemNotificationChannels.VIRTUAL_KEYBOARD)
-                        .setSmallIcon(com.android.internal.R.drawable.ic_notification_ime_default)
-                        .setWhen(0)
-                        .setOngoing(true)
-                        .addExtras(extras)
-                        .setCategory(Notification.CATEGORY_SYSTEM)
-                        .setColor(accentColor);
-
-        Intent intent = new Intent(ACTION_SHOW_INPUT_METHOD_PICKER)
-                .setPackage(mContext.getPackageName());
-        mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent,
-                PendingIntent.FLAG_IMMUTABLE);
-
         mShowOngoingImeSwitcherForPhones = false;
 
-        mNotificationShown = false;
         final int userId = mActivityManagerInternal.getCurrentUserId();
 
         mLastSwitchUserId = userId;
@@ -1939,7 +1890,6 @@
                 final int currentUserId = mSettings.getCurrentUserId();
                 mSettings.switchCurrentUser(currentUserId,
                         !mUserManagerInternal.isUserUnlockingOrUnlocked(currentUserId));
-                mNotificationManager = mContext.getSystemService(NotificationManager.class);
                 mStatusBarManagerInternal =
                         LocalServices.getService(StatusBarManagerInternal.class);
                 hideStatusBarIconLocked();
@@ -1977,7 +1927,6 @@
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_USER_ADDED);
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_USER_REMOVED);
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_LOCALE_CHANGED);
-                broadcastFilterForSystemUser.addAction(ACTION_SHOW_INPUT_METHOD_PICKER);
                 mContext.registerReceiver(new ImmsBroadcastReceiverForSystemUser(),
                         broadcastFilterForSystemUser);
 
@@ -3159,41 +3108,6 @@
                 mStatusBarManagerInternal.setImeWindowStatus(mCurTokenDisplayId,
                         getCurTokenLocked(), vis, backDisposition, needsToShowImeSwitcher);
             }
-            final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked());
-            if (imi != null && needsToShowImeSwitcher) {
-                // Used to load label
-                final CharSequence title = mRes.getText(
-                        com.android.internal.R.string.select_input_method);
-                final int currentUserId = mSettings.getCurrentUserId();
-                final Context userAwareContext = mContext.getUserId() == currentUserId
-                        ? mContext
-                        : mContext.createContextAsUser(UserHandle.of(currentUserId), 0 /* flags */);
-                final CharSequence summary = InputMethodUtils.getImeAndSubtypeDisplayName(
-                        userAwareContext, imi, mCurrentSubtype);
-                mImeSwitcherNotification.setContentTitle(title)
-                        .setContentText(summary)
-                        .setContentIntent(mImeSwitchPendingIntent);
-                // TODO(b/120076400): Figure out what is the best behavior
-                if ((mNotificationManager != null)
-                        && !mWindowManagerInternal.hasNavigationBar(DEFAULT_DISPLAY)) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "--- show notification: label =  " + summary);
-                    }
-                    mNotificationManager.notifyAsUser(null,
-                            SystemMessage.NOTE_SELECT_INPUT_METHOD,
-                            mImeSwitcherNotification.build(), UserHandle.ALL);
-                    mNotificationShown = true;
-                }
-            } else {
-                if (mNotificationShown && mNotificationManager != null) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "--- hide notification");
-                    }
-                    mNotificationManager.cancelAsUser(null,
-                            SystemMessage.NOTE_SELECT_INPUT_METHOD, UserHandle.ALL);
-                    mNotificationShown = false;
-                }
-            }
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index c7ff8ca..ebf9237d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -179,16 +179,6 @@
         }
     }
 
-    static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
-            InputMethodSubtype subtype) {
-        final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
-        return subtype != null
-                ? TextUtils.concat(subtype.getDisplayName(context,
-                        imi.getPackageName(), imi.getServiceInfo().applicationInfo),
-                                (TextUtils.isEmpty(imiLabel) ? "" : " - " + imiLabel))
-                : imiLabel;
-    }
-
     /**
      * Returns true if a package name belongs to a UID.
      *
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 77fea09..f459c0e 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -61,6 +61,7 @@
 import static android.content.pm.PackageManager.FEATURE_TELECOM;
 import static android.content.pm.PackageManager.FEATURE_TELEVISION;
 import static android.content.pm.PackageManager.MATCH_ALL;
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
@@ -10700,10 +10701,18 @@
         private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>();
         ArrayMap<Pair<ComponentName, Integer>, NotificationListenerFilter>
                 mRequestedNotificationListeners = new ArrayMap<>();
+        private final boolean mIsHeadlessSystemUserMode;
 
         public NotificationListeners(Context context, Object lock, UserProfiles userProfiles,
                 IPackageManager pm) {
+            this(context, lock, userProfiles, pm, UserManager.isHeadlessSystemUserMode());
+        }
+
+        @VisibleForTesting
+        public NotificationListeners(Context context, Object lock, UserProfiles userProfiles,
+                IPackageManager pm, boolean isHeadlessSystemUserMode) {
             super(context, lock, userProfiles, pm);
+            this.mIsHeadlessSystemUserMode = isHeadlessSystemUserMode;
         }
 
         @Override
@@ -10728,10 +10737,16 @@
                     if (TextUtils.isEmpty(listeners[i])) {
                         continue;
                     }
+                    int packageQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
+                    // In the headless system user mode, packages might not be installed for the
+                    // system user. Match packages for any user since apps can be installed only for
+                    // non-system users and would be considering uninstalled for the system user.
+                    if (mIsHeadlessSystemUserMode) {
+                        packageQueryFlags += MATCH_ANY_USER;
+                    }
                     ArraySet<ComponentName> approvedListeners =
-                            this.queryPackageForServices(listeners[i],
-                                    MATCH_DIRECT_BOOT_AWARE
-                                            | MATCH_DIRECT_BOOT_UNAWARE, USER_SYSTEM);
+                            this.queryPackageForServices(listeners[i], packageQueryFlags,
+                                    USER_SYSTEM);
                     for (int k = 0; k < approvedListeners.size(); k++) {
                         ComponentName cn = approvedListeners.valueAt(k);
                         addDefaultComponentOrPackage(cn.flattenToString());
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 8e672c3..17bb39c 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -166,6 +166,14 @@
         CollectionUtils.addAll(updatedTargets, removeOverlaysForUser(
                 (info) -> !userPackages.containsKey(info.packageName), newUserId));
 
+        final ArraySet<String> overlaidByOthers = new ArraySet<>();
+        for (AndroidPackage androidPackage : userPackages.values()) {
+            final String overlayTarget = androidPackage.getOverlayTarget();
+            if (!TextUtils.isEmpty(overlayTarget)) {
+                overlaidByOthers.add(overlayTarget);
+            }
+        }
+
         // Update the state of all installed packages containing overlays, and initialize new
         // overlays that are not currently in the settings.
         for (int i = 0, n = userPackages.size(); i < n; i++) {
@@ -175,8 +183,10 @@
                         updatePackageOverlays(pkg, newUserId, 0 /* flags */));
 
                 // When a new user is switched to for the first time, package manager must be
-                // informed of the overlay paths for all packages installed in the user.
-                updatedTargets.add(new PackageAndUser(pkg.getPackageName(), newUserId));
+                // informed of the overlay paths for all overlaid packages installed in the user.
+                if (overlaidByOthers.contains(pkg.getPackageName())) {
+                    updatedTargets.add(new PackageAndUser(pkg.getPackageName(), newUserId));
+                }
             } catch (OperationFailedException e) {
                 Slog.e(TAG, "failed to initialize overlays of '" + pkg.getPackageName()
                         + "' for user " + newUserId + "", e);
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index a4e295b..bf00a33 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -203,6 +203,12 @@
     boolean filterSharedLibPackage(@Nullable PackageStateInternal ps, int uid, int userId,
             long flags);
     boolean isCallerSameApp(String packageName, int uid);
+    /**
+     * Returns true if the package name and the uid represent the same app.
+     *
+     * @param resolveIsolatedUid if true, resolves an isolated uid into the real uid.
+     */
+    boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid);
     boolean isComponentVisibleToInstantApp(@Nullable ComponentName component);
     boolean isComponentVisibleToInstantApp(@Nullable ComponentName component,
             @PackageManager.ComponentType int type);
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 5d479d5..86b8272 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2209,11 +2209,19 @@
     }
 
     public final boolean isCallerSameApp(String packageName, int uid) {
+        return isCallerSameApp(packageName, uid, false /* resolveIsolatedUid */);
+    }
+
+    @Override
+    public final boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid) {
         if (Process.isSdkSandboxUid(uid)) {
             return (packageName != null
                     && packageName.equals(mService.getSdkSandboxPackageName()));
         }
         AndroidPackage pkg = mPackages.get(packageName);
+        if (resolveIsolatedUid && Process.isIsolated(uid)) {
+            uid = getIsolatedOwner(uid);
+        }
         return pkg != null
                 && UserHandle.getAppId(uid) == pkg.getUid();
     }
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 3f04264..c4f6836 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -18,6 +18,7 @@
 
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 
+import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
 import static com.android.server.pm.ApexManager.ActiveApexInfo;
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
 import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
@@ -34,6 +35,7 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.app.ActivityManager;
 import android.app.AppGlobals;
@@ -56,9 +58,16 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.art.ArtManagerLocal;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.pm.PackageDexOptimizer.DexOptResult;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
 import com.android.server.pm.pkg.PackageStateInternal;
 
 import dalvik.system.DexFile;
@@ -72,11 +81,15 @@
 import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 
-final class DexOptHelper {
+/**
+ * Helper class for dex optimization operations in PackageManagerService.
+ */
+public final class DexOptHelper {
     private static final long SEVEN_DAYS_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
 
     private final PackageManagerService mPm;
@@ -405,11 +418,12 @@
      * {@link PackageDexOptimizer#DEX_OPT_CANCELLED}
      * {@link PackageDexOptimizer#DEX_OPT_FAILED}
      */
-    @PackageDexOptimizer.DexOptResult
+    @DexOptResult
     /* package */ int performDexOptWithStatus(DexoptOptions options) {
         return performDexOptTraced(options);
     }
 
+    @DexOptResult
     private int performDexOptTraced(DexoptOptions options) {
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
         try {
@@ -421,7 +435,13 @@
 
     // Run dexopt on a given package. Returns true if dexopt did not fail, i.e.
     // if the package can now be considered up to date for the given filter.
+    @DexOptResult
     private int performDexOptInternal(DexoptOptions options) {
+        Optional<Integer> artSrvRes = performDexOptWithArtService(options);
+        if (artSrvRes.isPresent()) {
+            return artSrvRes.get();
+        }
+
         AndroidPackage p;
         PackageSetting pkgSetting;
         synchronized (mPm.mLock) {
@@ -446,8 +466,74 @@
         }
     }
 
-    private int performDexOptInternalWithDependenciesLI(AndroidPackage p,
-            @NonNull PackageStateInternal pkgSetting, DexoptOptions options) {
+    /**
+     * Performs dexopt on the given package using ART Service.
+     *
+     * @return a {@link DexOptResult}, or empty if the request isn't supported so that it is
+     *     necessary to fall back to the legacy code paths.
+     */
+    private Optional<Integer> performDexOptWithArtService(DexoptOptions options) {
+        ArtManagerLocal artManager = getArtManagerLocal();
+        if (artManager == null) {
+            return Optional.empty();
+        }
+
+        try (PackageManagerLocal.FilteredSnapshot snapshot =
+                        getPackageManagerLocal().withFilteredSnapshot()) {
+            PackageState ops = snapshot.getPackageState(options.getPackageName());
+            if (ops == null) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_FAILED);
+            }
+            AndroidPackage oap = ops.getAndroidPackage();
+            if (oap == null) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_FAILED);
+            }
+            if (oap.isApex()) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_SKIPPED);
+            }
+
+            // TODO(b/245301593): Delete the conditional when ART Service supports
+            // FLAG_SHOULD_INCLUDE_DEPENDENCIES and we can just set it unconditionally.
+            /*@OptimizeFlags*/ int extraFlags = ops.getUsesLibraries().isEmpty()
+                    ? 0
+                    : ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES;
+
+            OptimizeParams params = options.convertToOptimizeParams(extraFlags);
+            if (params == null) {
+                return Optional.empty();
+            }
+
+            // TODO(b/251903639): Either remove controlDexOptBlocking, or don't ignore it here.
+            OptimizeResult result;
+            try {
+                result = artManager.optimizePackage(snapshot, options.getPackageName(), params);
+            } catch (UnsupportedOperationException e) {
+                reportArtManagerFallback(options.getPackageName(), e.toString());
+                return Optional.empty();
+            }
+
+            // TODO(b/251903639): Move this to ArtManagerLocal.addOptimizePackageDoneCallback when
+            // it is implemented.
+            for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) {
+                PackageState ps = snapshot.getPackageState(pkgRes.getPackageName());
+                AndroidPackage ap = ps != null ? ps.getAndroidPackage() : null;
+                if (ap != null) {
+                    CompilerStats.PackageStats stats = mPm.getOrCreateCompilerPackageStats(ap);
+                    for (OptimizeResult.DexContainerFileOptimizeResult dexRes :
+                            pkgRes.getDexContainerFileOptimizeResults()) {
+                        stats.setCompileTime(
+                                dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis());
+                    }
+                }
+            }
+
+            return Optional.of(convertToDexOptResult(result));
+        }
+    }
+
+    @DexOptResult
+    private int performDexOptInternalWithDependenciesLI(
+            AndroidPackage p, @NonNull PackageStateInternal pkgSetting, DexoptOptions options) {
         // System server gets a special path.
         if (PLATFORM_PACKAGE_NAME.equals(p.getPackageName())) {
             return mPm.getDexManager().dexoptSystemServer(options);
@@ -514,10 +600,20 @@
 
         // Whoever is calling forceDexOpt wants a compiled package.
         // Don't use profiles since that may cause compilation to be skipped.
-        final int res = performDexOptInternalWithDependenciesLI(pkg, packageState,
-                new DexoptOptions(packageName, REASON_CMDLINE,
-                        getDefaultCompilerFilter(), null /* splitName */,
-                        DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE));
+        DexoptOptions options = new DexoptOptions(packageName, REASON_CMDLINE,
+                getDefaultCompilerFilter(), null /* splitName */,
+                DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE);
+
+        // performDexOptWithArtService ignores the snapshot and takes its own, so it can race with
+        // the package checks above, but at worst the effect is only a bit less friendly error
+        // below.
+        Optional<Integer> artSrvRes = performDexOptWithArtService(options);
+        int res;
+        if (artSrvRes.isPresent()) {
+            res = artSrvRes.get();
+        } else {
+            res = performDexOptInternalWithDependenciesLI(pkg, packageState, options);
+        }
 
         Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         if (res != PackageDexOptimizer.DEX_OPT_PERFORMED) {
@@ -800,4 +896,59 @@
         }
         return false;
     }
+
+    private @NonNull PackageManagerLocal getPackageManagerLocal() {
+        try {
+            return LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal.class);
+        } catch (ManagerNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Called whenever we need to fall back from ART Service to the legacy dexopt code.
+     */
+    public static void reportArtManagerFallback(String packageName, String reason) {
+        // STOPSHIP(b/251903639): Minimize these calls to avoid platform getting shipped with code
+        // paths that will always bypass ART Service.
+        Slog.i(TAG, "Falling back to old PackageManager dexopt for " + packageName + ": " + reason);
+    }
+
+    /**
+     * Returns {@link ArtManagerLocal} if one is found and should be used for package optimization.
+     */
+    private @Nullable ArtManagerLocal getArtManagerLocal() {
+        if (!"true".equals(SystemProperties.get("dalvik.vm.useartservice", ""))) {
+            return null;
+        }
+        try {
+            return LocalManagerRegistry.getManagerOrThrow(ArtManagerLocal.class);
+        } catch (ManagerNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Converts an ART Service {@link OptimizeResult} to {@link DexOptResult}.
+     *
+     * For interfacing {@link ArtManagerLocal} with legacy dex optimization code in PackageManager.
+     */
+    @DexOptResult
+    private static int convertToDexOptResult(OptimizeResult result) {
+        /*@OptimizeStatus*/ int status = result.getFinalStatus();
+        switch (status) {
+            case OptimizeResult.OPTIMIZE_SKIPPED:
+                return PackageDexOptimizer.DEX_OPT_SKIPPED;
+            case OptimizeResult.OPTIMIZE_FAILED:
+                return PackageDexOptimizer.DEX_OPT_FAILED;
+            case OptimizeResult.OPTIMIZE_PERFORMED:
+                return PackageDexOptimizer.DEX_OPT_PERFORMED;
+            case OptimizeResult.OPTIMIZE_CANCELLED:
+                return PackageDexOptimizer.DEX_OPT_CANCELLED;
+            default:
+                throw new IllegalArgumentException("OptimizeResult for "
+                        + result.getPackageOptimizeResults().get(0).getPackageName()
+                        + " has unsupported status " + status);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index d25bca7..2a2410fd 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -652,12 +652,6 @@
     @DexOptResult
     private int dexOptSecondaryDexPathLI(ApplicationInfo info, String path,
             PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) {
-        if (options.isDexoptOnlySharedDex() && !dexUseInfo.isUsedByOtherApps()) {
-            // We are asked to optimize only the dex files used by other apps and this is not
-            // on of them: skip it.
-            return DEX_OPT_SKIPPED;
-        }
-
         String compilerFilter = getRealCompilerFilter(info, options.getCompilerFilter(),
                 dexUseInfo.isUsedByOtherApps());
         // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags.
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 8fed153..6e54d0b 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -5242,25 +5242,30 @@
                 Map<String, String> classLoaderContextMap,
                 String loaderIsa) {
             int callingUid = Binder.getCallingUid();
-            if (PackageManagerService.PLATFORM_PACKAGE_NAME.equals(loadingPackageName)
-                    && callingUid != Process.SYSTEM_UID) {
+
+            // TODO(b/254043366): System server should not report its own dex load because there's
+            // nothing ART can do with it.
+
+            Computer snapshot = snapshot();
+
+            // System server should be able to report dex load on behalf of other apps. E.g., it
+            // could potentially resend the notifications in order to migrate the existing dex load
+            // info to ART Service.
+            if (!PackageManagerServiceUtils.isSystemOrRoot()
+                    && !snapshot.isCallerSameApp(
+                            loadingPackageName, callingUid, true /* resolveIsolatedUid */)) {
                 Slog.w(PackageManagerService.TAG,
-                        "Non System Server process reporting dex loads as system server. uid="
-                                + callingUid);
-                // Do not record dex loads from processes pretending to be system server.
-                // Only the system server should be assigned the package "android", so reject calls
-                // that don't satisfy the constraint.
-                //
-                // notifyDexLoad is a PM API callable from the app process. So in theory, apps could
-                // craft calls to this API and pretend to be system server. Doing so poses no
-                // particular danger for dex load reporting or later dexopt, however it is a
-                // sensible check to do in order to verify the expectations.
+                        TextUtils.formatSimple(
+                                "Invalid dex load report. loadingPackageName=%s, uid=%d",
+                                loadingPackageName, callingUid));
                 return;
             }
 
+            // TODO(b/254043366): Call `ArtManagerLocal.notifyDexLoad`.
+
             int userId = UserHandle.getCallingUserId();
-            ApplicationInfo ai = snapshot().getApplicationInfo(loadingPackageName, /*flags*/ 0,
-                    userId);
+            ApplicationInfo ai =
+                    snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId);
             if (ai == null) {
                 Slog.w(PackageManagerService.TAG, "Loading a package that does not exist for the calling user. package="
                         + loadingPackageName + ", user=" + userId);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 60f2478..2119191 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2633,6 +2633,9 @@
     /** @return a specific user restriction that's in effect currently. */
     @Override
     public boolean hasUserRestriction(String restrictionKey, @UserIdInt int userId) {
+        if (!userExists(userId)) {
+            return false;
+        }
         checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "hasUserRestriction");
         return mLocalService.hasUserRestriction(restrictionKey, userId);
     }
@@ -5516,6 +5519,13 @@
 
     private void removeUserState(final @UserIdInt int userId) {
         Slog.i(LOG_TAG, "Removing user state of user " + userId);
+
+        // Cleanup lock settings.  This must happen before destroyUserKey(), since the user's DE
+        // storage must still be accessible for the lock settings state to be properly cleaned up.
+        mLockPatternUtils.removeUser(userId);
+
+        // Evict and destroy the user's CE and DE encryption keys.  At this point, the user's CE and
+        // DE storage is made inaccessible, except to delete its contents.
         try {
             mContext.getSystemService(StorageManager.class).destroyUserKey(userId);
         } catch (IllegalStateException e) {
@@ -5523,9 +5533,6 @@
             Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e);
         }
 
-        // Cleanup lock settings
-        mLockPatternUtils.removeUser(userId);
-
         // Cleanup package manager settings
         mPm.cleanUpUser(this, userId);
 
diff --git a/services/core/java/com/android/server/pm/dex/DexoptOptions.java b/services/core/java/com/android/server/pm/dex/DexoptOptions.java
index ea23316..f5557c4 100644
--- a/services/core/java/com/android/server/pm/dex/DexoptOptions.java
+++ b/services/core/java/com/android/server/pm/dex/DexoptOptions.java
@@ -18,6 +18,16 @@
 
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
 
+import android.annotation.Nullable;
+
+import com.android.server.art.ReasonMapping;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.pm.DexOptHelper;
+import com.android.server.pm.PackageManagerService;
+
+import dalvik.system.DexFile;
+
 /**
  * Options used for dexopt invocations.
  */
@@ -40,10 +50,6 @@
     // will only consider the primary apk.
     public static final int DEXOPT_ONLY_SECONDARY_DEX = 1 << 3;
 
-    // When set, dexopt will optimize only dex files that are used by other apps.
-    // Currently, this flag is ignored for primary apks.
-    public static final int DEXOPT_ONLY_SHARED_DEX = 1 << 4;
-
     // When set, dexopt will attempt to scale down the optimizations previously applied in order
     // save disk space.
     public static final int DEXOPT_DOWNGRADE = 1 << 5;
@@ -105,7 +111,6 @@
                 DEXOPT_FORCE |
                 DEXOPT_BOOT_COMPLETE |
                 DEXOPT_ONLY_SECONDARY_DEX |
-                DEXOPT_ONLY_SHARED_DEX |
                 DEXOPT_DOWNGRADE |
                 DEXOPT_AS_SHARED_LIBRARY |
                 DEXOPT_IDLE_BACKGROUND_JOB |
@@ -146,10 +151,6 @@
         return (mFlags & DEXOPT_ONLY_SECONDARY_DEX) != 0;
     }
 
-    public boolean isDexoptOnlySharedDex() {
-        return (mFlags & DEXOPT_ONLY_SHARED_DEX) != 0;
-    }
-
     public boolean isDowngrade() {
         return (mFlags & DEXOPT_DOWNGRADE) != 0;
     }
@@ -198,4 +199,133 @@
                 mSplitName,
                 mFlags);
     }
+
+    /**
+     * Returns an {@link OptimizeParams} instance corresponding to this object, for use with
+     * {@link com.android.server.art.ArtManagerLocal}.
+     *
+     * @param extraFlags extra {@link ArtFlags#OptimizeFlags} to set in the returned
+     *     {@code OptimizeParams} beyond those converted from this object
+     * @return null if the settings cannot be accurately represented, and hence the old
+     *     PackageManager/installd code paths need to be used.
+     */
+    public @Nullable OptimizeParams convertToOptimizeParams(/*@OptimizeFlags*/ int extraFlags) {
+        if (mSplitName != null) {
+            DexOptHelper.reportArtManagerFallback(
+                    mPackageName, "Request to optimize only split " + mSplitName);
+            return null;
+        }
+
+        /*@OptimizeFlags*/ int flags = extraFlags;
+        if ((mFlags & DEXOPT_CHECK_FOR_PROFILES_UPDATES) == 0
+                && DexFile.isProfileGuidedCompilerFilter(mCompilerFilter)) {
+            // ART Service doesn't support bypassing this, so not setting this flag is not
+            // supported.
+            DexOptHelper.reportArtManagerFallback(mPackageName,
+                    "DEXOPT_CHECK_FOR_PROFILES_UPDATES not set with profile compiler filter");
+            return null;
+        }
+        if ((mFlags & DEXOPT_FORCE) != 0) {
+            flags |= ArtFlags.FLAG_FORCE;
+        }
+        if ((mFlags & DEXOPT_ONLY_SECONDARY_DEX) != 0) {
+            flags |= ArtFlags.FLAG_FOR_SECONDARY_DEX;
+        } else {
+            flags |= ArtFlags.FLAG_FOR_PRIMARY_DEX;
+        }
+        if ((mFlags & DEXOPT_DOWNGRADE) != 0) {
+            flags |= ArtFlags.FLAG_SHOULD_DOWNGRADE;
+        }
+        if ((mFlags & DEXOPT_INSTALL_WITH_DEX_METADATA_FILE) == 0) {
+            // ART Service cannot be instructed to ignore a DM file if present, so not setting this
+            // flag is not supported.
+            DexOptHelper.reportArtManagerFallback(
+                    mPackageName, "DEXOPT_INSTALL_WITH_DEX_METADATA_FILE not set");
+            return null;
+        }
+
+        /*@PriorityClassApi*/ int priority;
+        // Replicates logic in RunDex2Oat::PrepareCompilerRuntimeAndPerfConfigFlags in installd.
+        if ((mFlags & DEXOPT_BOOT_COMPLETE) != 0) {
+            if ((mFlags & DEXOPT_FOR_RESTORE) != 0) {
+                priority = ArtFlags.PRIORITY_INTERACTIVE_FAST;
+            } else {
+                // TODO(b/251903639): Repurpose DEXOPT_IDLE_BACKGROUND_JOB to choose new
+                // dalvik.vm.background-dex2oat-* properties.
+                priority = ArtFlags.PRIORITY_INTERACTIVE;
+            }
+        } else {
+            priority = ArtFlags.PRIORITY_BOOT;
+        }
+
+        // The following flags in mFlags are ignored:
+        //
+        // -  DEXOPT_AS_SHARED_LIBRARY: It's implicit with ART Service since it always looks at
+        //    <uses-library> rather than actual dependencies.
+        //
+        //    We don't require it to be set either. It's safe when switching between old and new
+        //    code paths since the only effect is that some packages may be unnecessarily compiled
+        //    without user profiles.
+        //
+        // -  DEXOPT_IDLE_BACKGROUND_JOB: Its only effect is to allow the debug variant dex2oatd to
+        //    be used, but ART Service never uses that (cf. Artd::GetDex2Oat in artd.cc).
+
+        String reason;
+        switch (mCompilationReason) {
+            case PackageManagerService.REASON_FIRST_BOOT:
+                reason = ReasonMapping.REASON_FIRST_BOOT;
+                break;
+            case PackageManagerService.REASON_BOOT_AFTER_OTA:
+                reason = ReasonMapping.REASON_BOOT_AFTER_OTA;
+                break;
+            case PackageManagerService.REASON_POST_BOOT:
+                // This reason will go away with the legacy dexopt code.
+                DexOptHelper.reportArtManagerFallback(
+                        mPackageName, "Unsupported compilation reason REASON_POST_BOOT");
+                return null;
+            case PackageManagerService.REASON_INSTALL:
+                reason = ReasonMapping.REASON_INSTALL;
+                break;
+            case PackageManagerService.REASON_INSTALL_FAST:
+                reason = ReasonMapping.REASON_INSTALL_FAST;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK:
+                reason = ReasonMapping.REASON_INSTALL_BULK;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_SECONDARY:
+                reason = ReasonMapping.REASON_INSTALL_BULK_SECONDARY;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_DOWNGRADED:
+                reason = ReasonMapping.REASON_INSTALL_BULK_DOWNGRADED;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED:
+                reason = ReasonMapping.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED;
+                break;
+            case PackageManagerService.REASON_BACKGROUND_DEXOPT:
+                reason = ReasonMapping.REASON_BG_DEXOPT;
+                break;
+            case PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE:
+                reason = ReasonMapping.REASON_INACTIVE;
+                break;
+            case PackageManagerService.REASON_CMDLINE:
+                reason = ReasonMapping.REASON_CMDLINE;
+                break;
+            case PackageManagerService.REASON_SHARED:
+            case PackageManagerService.REASON_AB_OTA:
+                // REASON_SHARED shouldn't go into this code path - it's only used at lower levels
+                // in PackageDexOptimizer.
+                // TODO(b/251921228): OTA isn't supported, so REASON_AB_OTA shouldn't come this way
+                // either.
+                throw new UnsupportedOperationException(
+                        "ART Service unsupported compilation reason " + mCompilationReason);
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid compilation reason " + mCompilationReason);
+        }
+
+        return new OptimizeParams.Builder(reason, flags)
+                .setCompilerFilter(mCompilerFilter)
+                .setPriorityClass(priority)
+                .build();
+    }
 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index d39b649..a6fac4d 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -114,6 +114,7 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Rect;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.hdmi.HdmiAudioSystemClient;
@@ -391,6 +392,7 @@
     IStatusBarService mStatusBarService;
     StatusBarManagerInternal mStatusBarManagerInternal;
     AudioManagerInternal mAudioManagerInternal;
+    SensorPrivacyManager mSensorPrivacyManager;
     DisplayManager mDisplayManager;
     DisplayManagerInternal mDisplayManagerInternal;
     boolean mPreloadedRecentApps;
@@ -1912,6 +1914,7 @@
         mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class);
         mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
         mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+        mSensorPrivacyManager = mContext.getSystemService(SensorPrivacyManager.class);
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
         mPackageManager = mContext.getPackageManager();
@@ -3079,6 +3082,18 @@
         return key_not_consumed;
     }
 
+    private void toggleMicrophoneMuteFromKey() {
+        if (mSensorPrivacyManager.supportsSensorToggle(
+                SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
+                SensorPrivacyManager.Sensors.MICROPHONE)) {
+            boolean isEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
+                    SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
+                    SensorPrivacyManager.Sensors.MICROPHONE);
+            mSensorPrivacyManager.setSensorPrivacy(SensorPrivacyManager.Sensors.MICROPHONE,
+                    !isEnabled);
+        }
+    }
+
     /**
      * TV only: recognizes a remote control gesture for capturing a bug report.
      */
@@ -4011,11 +4026,16 @@
                 break;
             }
 
+            case KeyEvent.KEYCODE_MUTE:
+                result &= ~ACTION_PASS_TO_USER;
+                if (down && event.getRepeatCount() == 0) {
+                    toggleMicrophoneMuteFromKey();
+                }
+                break;
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
             case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
@@ -4193,7 +4213,9 @@
         if (mRequestedOrSleepingDefaultDisplay) {
             mCameraGestureTriggeredDuringGoingToSleep = true;
             // Wake device up early to prevent display doing redundant turning off/on stuff.
-            wakeUpFromPowerKey(event.getDownTime());
+            wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey,
+                    PowerManager.WAKE_REASON_CAMERA_LAUNCH,
+                    "android.policy:CAMERA_GESTURE_PREVENT_LOCK");
         }
         return true;
     }
@@ -4726,11 +4748,6 @@
             }
             mDefaultDisplayRotation.updateOrientationListener();
             reportScreenStateToVrManager(false);
-            if (mCameraGestureTriggeredDuringGoingToSleep) {
-                wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey,
-                        PowerManager.WAKE_REASON_CAMERA_LAUNCH,
-                        "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK");
-            }
         }
     }
 
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 5abc875..d8b1120 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -42,8 +42,6 @@
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.SynchronousUserSwitchObserver;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -64,7 +62,6 @@
 import android.os.BatteryManagerInternal;
 import android.os.BatterySaverPolicyConfig;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.IBinder;
@@ -127,7 +124,6 @@
 import com.android.server.UserspaceRebootLogger;
 import com.android.server.Watchdog;
 import com.android.server.am.BatteryStatsService;
-import com.android.server.compat.PlatformCompat;
 import com.android.server.lights.LightsManager;
 import com.android.server.lights.LogicalLight;
 import com.android.server.policy.WindowManagerPolicy;
@@ -284,17 +280,6 @@
      */
     private static final long ENHANCED_DISCHARGE_PREDICTION_BROADCAST_MIN_DELAY_MS = 60 * 1000L;
 
-    /**
-     * Apps targeting Android U and above need to define
-     * {@link android.Manifest.permission#TURN_SCREEN_ON} in their manifest for
-     * {@link android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP} to have any effect.
-     * Note that most applications should use {@link android.R.attr#turnScreenOn} or
-     * {@link android.app.Activity#setTurnScreenOn(boolean)} instead, as this prevents the
-     * previous foreground app from being resumed first when the screen turns on.
-     */
-    @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public static final long REQUIRE_TURN_SCREEN_ON_PERMISSION = 216114297L;
     /** Reason ID for holding display suspend blocker. */
     private static final String HOLDING_DISPLAY_SUSPEND_BLOCKER = "holding display";
 
@@ -318,7 +303,6 @@
     private final SystemPropertiesWrapper mSystemProperties;
     private final Clock mClock;
     private final Injector mInjector;
-    private final PlatformCompat mPlatformCompat;
 
     private AppOpsManager mAppOpsManager;
     private LightsManager mLightsManager;
@@ -1012,11 +996,6 @@
                 public void set(String key, String val) {
                     SystemProperties.set(key, val);
                 }
-
-                @Override
-                public boolean getBoolean(String key, boolean def) {
-                    return SystemProperties.getBoolean(key, def);
-                }
             };
         }
 
@@ -1053,10 +1032,6 @@
         AppOpsManager createAppOpsManager(Context context) {
             return context.getSystemService(AppOpsManager.class);
         }
-
-        PlatformCompat createPlatformCompat(Context context) {
-            return context.getSystemService(PlatformCompat.class);
-        }
     }
 
     final Constants mConstants;
@@ -1114,8 +1089,6 @@
 
         mAppOpsManager = injector.createAppOpsManager(mContext);
 
-        mPlatformCompat = injector.createPlatformCompat(mContext);
-
         mPowerGroupWakefulnessChangeListener = new PowerGroupWakefulnessChangeListener();
 
         // Save brightness values:
@@ -1626,28 +1599,14 @@
         }
         if (mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, opUid, opPackageName)
                 == AppOpsManager.MODE_ALLOWED) {
-            if (mPlatformCompat.isChangeEnabledByPackageName(REQUIRE_TURN_SCREEN_ON_PERMISSION,
-                    opPackageName, UserHandle.getUserId(opUid))) {
-                if (mContext.checkCallingOrSelfPermission(
-                        android.Manifest.permission.TURN_SCREEN_ON)
-                        == PackageManager.PERMISSION_GRANTED) {
-                    if (DEBUG_SPEW) {
-                        Slog.d(TAG, "Allowing device wake-up from app " + opPackageName);
-                    }
-                    return true;
-                }
-            } else {
-                // android.permission.TURN_SCREEN_ON has only been introduced in Android U, only
-                // check for appOp for apps targeting lower SDK versions
-                if (DEBUG_SPEW) {
-                    Slog.d(TAG, "Allowing device wake-up from app with "
-                            + "REQUIRE_TURN_SCREEN_ON_PERMISSION disabled " + opPackageName);
-                }
+            if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.TURN_SCREEN_ON)
+                    == PackageManager.PERMISSION_GRANTED) {
+                Slog.i(TAG, "Allowing device wake-up from app " + opPackageName);
                 return true;
             }
         }
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
-            Slog.d(TAG, "Device wake-up will be denied without android.permission.TURN_SCREEN_ON");
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
+            Slog.d(TAG, "Device wake-up allowed by debug.power.permissionless_turn_screen_on");
             return true;
         }
         Slog.w(TAG, "Not allowing device wake-up for " + opPackageName);
@@ -6768,6 +6727,11 @@
         public void nap(long eventTime, boolean allowWake) {
             napInternal(eventTime, Process.SYSTEM_UID, allowWake);
         }
+
+        @Override
+        public boolean isAmbientDisplaySuppressed() {
+            return mAmbientDisplaySuppressionController.isSuppressed();
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
index c68f9c6..1acf798 100644
--- a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
+++ b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
@@ -48,19 +48,4 @@
      * SELinux. libc will log the underlying reason.
      */
     void set(@NonNull String key, @Nullable String val);
-
-    /**
-     * Get the value for the given {@code key}, returned as a boolean.
-     * Values 'n', 'no', '0', 'false' or 'off' are considered false.
-     * Values 'y', 'yes', '1', 'true' or 'on' are considered true.
-     * (case sensitive).
-     * If the key does not exist, or has any other value, then the default
-     * result is returned.
-     *
-     * @param key the key to lookup
-     * @param def a default value to return
-     * @return the key parsed as a boolean, or def if the key isn't found or is
-     *         not able to be parsed as a boolean.
-     */
-    boolean getBoolean(@NonNull String key, boolean def);
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 7ccf85f..d378b11 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -2272,6 +2272,25 @@
 
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+        boolean proto = false;
+        for (int i = 0; i < args.length; i++) {
+            if ("--proto".equals(args[i])) {
+                proto = true;
+            }
+        }
+        if (proto) {
+            if (mBar == null)  return;
+            try (TransferPipe tp = new TransferPipe()) {
+                // Sending the command to the remote, which needs to execute async to avoid blocking
+                // See Binder#dumpAsync() for inspiration
+                mBar.dumpProto(args, tp.getWriteFd());
+                // Times out after 5s
+                tp.go(fd);
+            } catch (Throwable t) {
+                Slog.e(TAG, "Error sending command to IStatusBar", t);
+            }
+            return;
+        }
 
         synchronized (mLock) {
             for (int i = 0; i < mDisplayUiState.size(); i++) {
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 8ac4fd4..141be70 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -65,6 +65,9 @@
     public final DeviceVibrationEffectAdapter deviceEffectAdapter;
     public final VibrationThread.VibratorManagerHooks vibratorManagerHooks;
 
+    // Not guarded by lock because they're not modified by this conductor, it's used here only to
+    // check immutable attributes. The status and other mutable states are changed by the service or
+    // by the vibrator steps.
     private final Vibration mVibration;
     private final SparseArray<VibratorController> mVibrators = new SparseArray<>();
 
@@ -412,6 +415,16 @@
         }
     }
 
+    /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */
+    public boolean wasNotifiedToCancel() {
+        if (Build.IS_DEBUGGABLE) {
+            expectIsVibrationThread(false);
+        }
+        synchronized (mLock) {
+            return mSignalCancel != null;
+        }
+    }
+
     @GuardedBy("mLock")
     private boolean hasPendingNotifySignalLocked() {
         if (Build.IS_DEBUGGABLE) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 8514e27..8613b50 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -864,8 +864,8 @@
         }
 
         Vibration currentVibration = mCurrentVibration.getVibration();
-        if (currentVibration.hasEnded()) {
-            // Current vibration is finishing up, it should not block incoming vibrations.
+        if (currentVibration.hasEnded() || mCurrentVibration.wasNotifiedToCancel()) {
+            // Current vibration has ended or is cancelling, should not block incoming vibrations.
             return null;
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2232aa1..81bb3a1 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8454,7 +8454,7 @@
         getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
                 mCompatDisplayInsets);
         // Use current screen layout as source because the size of app is independent to parent.
-        resolvedConfig.screenLayout = TaskFragment.computeScreenLayoutOverride(
+        resolvedConfig.screenLayout = computeScreenLayout(
                 getConfiguration().screenLayout, resolvedConfig.screenWidthDp,
                 resolvedConfig.screenHeightDp);
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 214a2c1..3c457e1 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2578,6 +2578,9 @@
                         // activity lifecycle transaction to make sure the override pending app
                         // transition will be applied immediately.
                         targetActivity.applyOptionsAnimation();
+                        if (activityOptions != null && activityOptions.getLaunchCookie() != null) {
+                            targetActivity.mLaunchCookie = activityOptions.getLaunchCookie();
+                        }
                     } finally {
                         mActivityMetricsLogger.notifyActivityLaunched(launchingState,
                                 START_TASK_TO_FRONT, false /* newActivityCreated */,
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e977447..30399ed 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -588,6 +588,7 @@
 
         ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
                 "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity);
+        activity.mTaskSupervisor.mStoppingActivities.remove(activity);
         activity.getDisplayContent().ensureActivitiesVisible(null /* starting */,
                 0 /* configChanges */, false /* preserveWindows */, true);
     }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 3c847ce..739f41f 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2188,8 +2188,7 @@
             mDisplayInfo.flags &= ~Display.FLAG_SCALING_DISABLED;
         }
 
-        computeSizeRangesAndScreenLayout(mDisplayInfo, rotated, dw, dh,
-                mDisplayMetrics.density, outConfig);
+        computeSizeRanges(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig);
 
         mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId,
                 mDisplayInfo);
@@ -2289,8 +2288,7 @@
         displayInfo.appHeight = appBounds.height();
         final DisplayCutout displayCutout = calculateDisplayCutoutForRotation(rotation);
         displayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout;
-        computeSizeRangesAndScreenLayout(displayInfo, rotated, dw, dh,
-                mDisplayMetrics.density, outConfig);
+        computeSizeRanges(displayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig);
         return displayInfo;
     }
 
@@ -2309,6 +2307,9 @@
         outConfig.screenHeightDp = (int) (info.mConfigFrame.height() / density + 0.5f);
         outConfig.compatScreenWidthDp = (int) (outConfig.screenWidthDp / mCompatibleScreenScale);
         outConfig.compatScreenHeightDp = (int) (outConfig.screenHeightDp / mCompatibleScreenScale);
+        outConfig.screenLayout = computeScreenLayout(
+                Configuration.resetScreenLayout(outConfig.screenLayout),
+                outConfig.screenWidthDp, outConfig.screenHeightDp);
 
         final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
         outConfig.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, dw, dh);
@@ -2450,7 +2451,7 @@
         return curSize;
     }
 
-    private void computeSizeRangesAndScreenLayout(DisplayInfo displayInfo, boolean rotated,
+    private void computeSizeRanges(DisplayInfo displayInfo, boolean rotated,
             int dw, int dh, float density, Configuration outConfig) {
 
         // We need to determine the smallest width that will occur under normal
@@ -2477,31 +2478,8 @@
         if (outConfig == null) {
             return;
         }
-        int sl = Configuration.resetScreenLayout(outConfig.screenLayout);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_0, density, unrotDw, unrotDh);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_90, density, unrotDh, unrotDw);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_180, density, unrotDw, unrotDh);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_270, density, unrotDh, unrotDw);
         outConfig.smallestScreenWidthDp =
                 (int) (displayInfo.smallestNominalAppWidth / density + 0.5f);
-        outConfig.screenLayout = sl;
-    }
-
-    private int reduceConfigLayout(int curLayout, int rotation, float density, int dw, int dh) {
-        // Get the app screen size at this rotation.
-        final Rect size = mDisplayPolicy.getDecorInsetsInfo(rotation, dw, dh).mNonDecorFrame;
-
-        // Compute the screen layout size class for this rotation.
-        int longSize = size.width();
-        int shortSize = size.height();
-        if (longSize < shortSize) {
-            int tmp = longSize;
-            longSize = shortSize;
-            shortSize = tmp;
-        }
-        longSize = (int) (longSize / density + 0.5f);
-        shortSize = (int) (shortSize / density + 0.5f);
-        return Configuration.reduceScreenLayout(curLayout, longSize, shortSize);
     }
 
     private void adjustDisplaySizeRanges(DisplayInfo displayInfo, int rotation, int dw, int dh) {
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 5505539..449e77f 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -55,6 +55,7 @@
 import android.window.ScreenCapture;
 
 import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.display.DisplayControl;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
@@ -246,7 +247,7 @@
             HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
                     "ScreenRotationAnimation#getMedianBorderLuma");
-            mStartLuma = RotationAnimationUtils.getMedianBorderLuma(hardwareBuffer,
+            mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer,
                     screenshotBuffer.getColorSpace());
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
 
@@ -489,8 +490,8 @@
             return false;
         }
         if (!mStarted) {
-            mEndLuma = RotationAnimationUtils.getLumaOfSurfaceControl(mDisplayContent.getDisplay(),
-                    mDisplayContent.getWindowingLayer());
+            mEndLuma = TransitionAnimation.getBorderLuma(mDisplayContent.getWindowingLayer(),
+                    finalWidth, finalHeight);
             startAnimation(t, maxAnimationDuration, animationScale, finalWidth, finalHeight,
                     exitAnim, enterAnim);
         }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 885968f..391d081 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -22,7 +22,6 @@
 import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -5787,12 +5786,10 @@
             return false;
         }
 
-        // Existing Tasks can be reused if a new root task will be created anyway, or for the
-        // Dream - because there can only ever be one DreamActivity.
+        // Existing Tasks can be reused if a new root task will be created anyway.
         final int windowingMode = getWindowingMode();
         final int activityType = getActivityType();
-        return DisplayContent.alwaysCreateRootTask(windowingMode, activityType)
-                || activityType == ACTIVITY_TYPE_DREAM;
+        return DisplayContent.alwaysCreateRootTask(windowingMode, activityType);
     }
 
     void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index efb6302..230b760 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2189,7 +2189,7 @@
                 compatScreenHeightDp = inOutConfig.screenHeightDp;
             }
             // Reducing the screen layout starting from its parent config.
-            inOutConfig.screenLayout = computeScreenLayoutOverride(parentConfig.screenLayout,
+            inOutConfig.screenLayout = computeScreenLayout(parentConfig.screenLayout,
                     compatScreenWidthDp, compatScreenHeightDp);
         }
     }
@@ -2252,16 +2252,6 @@
         }
     }
 
-    /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */
-    static int computeScreenLayoutOverride(int sourceScreenLayout, int screenWidthDp,
-            int screenHeightDp) {
-        sourceScreenLayout = sourceScreenLayout
-                & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
-        final int longSize = Math.max(screenWidthDp, screenHeightDp);
-        final int shortSize = Math.min(screenWidthDp, screenHeightDp);
-        return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize);
-    }
-
     @Override
     public int getActivityType() {
         final int applicationType = super.getActivityType();
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 4459d45..b2c8b7a 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -83,11 +83,11 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.inputmethod.InputMethodManagerInternal;
-import com.android.server.wm.utils.RotationAnimationUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -2190,7 +2190,7 @@
             changeInfo.mSnapshot = snapshotSurface;
             if (isDisplayRotation) {
                 // This isn't cheap, so only do it for display rotations.
-                changeInfo.mSnapshotLuma = RotationAnimationUtils.getMedianBorderLuma(
+                changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma(
                         screenshotBuffer.getHardwareBuffer(), screenshotBuffer.getColorSpace());
             }
             SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get();
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 9763df6..c4c66d8 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1607,6 +1607,16 @@
         return false;
     }
 
+    /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */
+    static int computeScreenLayout(int sourceScreenLayout, int screenWidthDp,
+            int screenHeightDp) {
+        sourceScreenLayout = sourceScreenLayout
+                & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
+        final int longSize = Math.max(screenWidthDp, screenHeightDp);
+        final int shortSize = Math.min(screenWidthDp, screenHeightDp);
+        return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize);
+    }
+
     // TODO: Users would have their own window containers under the display container?
     void switchUser(int userId) {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 32feb6c..c206a15 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -613,15 +613,6 @@
             @NonNull IBinder imeTargetWindowToken);
 
     /**
-     * Returns the presence of a software navigation bar on the specified display.
-     *
-     * @param displayId the id of display to check if there is a software navigation bar.
-     * @return {@code true} if there is a software navigation. {@code false} otherwise, including
-     *         the case when the specified display does not exist.
-     */
-    public abstract boolean hasNavigationBar(int displayId);
-
-    /**
       * Returns true when the hardware keyboard is available.
       */
     public abstract boolean isHardKeyboardAvailable();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index c17af30..c9d3dac 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7917,11 +7917,6 @@
         }
 
         @Override
-        public boolean hasNavigationBar(int displayId) {
-            return WindowManagerService.this.hasNavigationBar(displayId);
-        }
-
-        @Override
         public boolean isHardKeyboardAvailable() {
             synchronized (mGlobalLock) {
                 return mHardKeyboardAvailable;
@@ -8703,11 +8698,12 @@
         h.ownerPid = callingPid;
 
         if (region == null) {
-            h.replaceTouchableRegionWithCrop = true;
+            h.replaceTouchableRegionWithCrop(null);
         } else {
             h.touchableRegion.set(region);
+            h.replaceTouchableRegionWithCrop = false;
+            h.setTouchableRegionCrop(surface);
         }
-        h.setTouchableRegionCrop(null /* use the input surface's bounds */);
 
         final SurfaceControl.Transaction t = mTransactionFactory.get();
         t.setInputWindowInfo(surface, h);
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 8055590..7c481f5 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -448,14 +448,8 @@
         if (mFixedRotationTransformState != null) {
             mFixedRotationTransformState.disassociate(this);
         }
-        // TODO(b/233855302): Remove TaskFragment override if the DisplayContent uses the same
-        //  bounds for screenLayout calculation.
-        final Configuration overrideConfig = new Configuration(config);
-        overrideConfig.screenLayout = TaskFragment.computeScreenLayoutOverride(
-                overrideConfig.screenLayout, overrideConfig.screenWidthDp,
-                overrideConfig.screenHeightDp);
         mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames,
-                overrideConfig, mDisplayContent.getRotation());
+                new Configuration(config), mDisplayContent.getRotation());
         mFixedRotationTransformState.mAssociatedTokens.add(this);
         mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames);
         onFixedRotationStatePrepared();
diff --git a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
index b93b8d8..c11a6d0 100644
--- a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
+++ b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
@@ -16,24 +16,11 @@
 
 package com.android.server.wm.utils;
 
-import static android.hardware.HardwareBuffer.RGBA_8888;
 import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
 
-import android.graphics.Color;
-import android.graphics.ColorSpace;
 import android.graphics.Matrix;
-import android.graphics.Point;
-import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.media.Image;
-import android.media.ImageReader;
-import android.view.Display;
 import android.view.Surface;
-import android.view.SurfaceControl;
-import android.window.ScreenCapture;
-
-import java.nio.ByteBuffer;
-import java.util.Arrays;
 
 
 /** Helper functions for the {@link com.android.server.wm.ScreenRotationAnimation} class*/
@@ -46,89 +33,6 @@
         return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
     }
 
-    /**
-     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
-     * luminance at the borders of the bitmap
-     * @return the average luminance of all the pixels at the borders of the bitmap
-     */
-    public static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
-        // Cannot read content from buffer with protected usage.
-        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
-                || hasProtectedContent(hardwareBuffer)) {
-            return 0;
-        }
-
-        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
-                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
-        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
-        Image image = ir.acquireLatestImage();
-        if (image == null || image.getPlanes().length == 0) {
-            return 0;
-        }
-
-        Image.Plane plane = image.getPlanes()[0];
-        ByteBuffer buffer = plane.getBuffer();
-        int width = image.getWidth();
-        int height = image.getHeight();
-        int pixelStride = plane.getPixelStride();
-        int rowStride = plane.getRowStride();
-        float[] borderLumas = new float[2 * width + 2 * height];
-
-        // Grab the top and bottom borders
-        int l = 0;
-        for (int x = 0; x < width; x++) {
-            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
-        }
-
-        // Grab the left and right borders
-        for (int y = 0; y < height; y++) {
-            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
-        }
-
-        // Cleanup
-        ir.close();
-
-        // Oh, is this too simple and inefficient for you?
-        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
-        Arrays.sort(borderLumas);
-        return borderLumas[borderLumas.length / 2];
-    }
-
-    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
-            int pixelStride, int rowStride) {
-        int offset = y * rowStride + x * pixelStride;
-        int pixel = 0;
-        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
-        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
-        pixel |= (buffer.get(offset + 2) & 0xff);       // B
-        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
-        return Color.valueOf(pixel).luminance();
-    }
-
-    /**
-     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
-     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
-     */
-    public static float getLumaOfSurfaceControl(Display display, SurfaceControl surfaceControl) {
-        if (surfaceControl ==  null) {
-            return 0;
-        }
-
-        Point size = new Point();
-        display.getSize(size);
-        Rect crop = new Rect(0, 0, size.x, size.y);
-        ScreenCapture.ScreenshotHardwareBuffer buffer =
-                ScreenCapture.captureLayers(surfaceControl, crop, 1);
-        if (buffer == null) {
-            return 0;
-        }
-
-        return RotationAnimationUtils.getMedianBorderLuma(buffer.getHardwareBuffer(),
-                buffer.getColorSpace());
-    }
-
     public static void createRotationMatrix(int rotation, int width, int height, Matrix outMatrix) {
         switch (rotation) {
             case Surface.ROTATION_0:
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 91f5c69..352a257 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -49,7 +49,8 @@
 
     public CredentialManagerService(@NonNull Context context) {
         super(context,
-                new SecureSettingsServiceNameResolver(context, Settings.Secure.AUTOFILL_SERVICE),
+                new SecureSettingsServiceNameResolver(context, Settings.Secure.AUTOFILL_SERVICE,
+                        /*isMultiple=*/true),
                 null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER);
     }
 
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
index f45f626..aa19241 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
@@ -17,6 +17,11 @@
 package com.android.server.credentials;
 
 import android.annotation.NonNull;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.server.infra.AbstractPerUserSystemService;
@@ -24,7 +29,7 @@
 /**
  * Per-user implementation of {@link CredentialManagerService}
  */
-public class CredentialManagerServiceImpl extends
+public final class CredentialManagerServiceImpl extends
         AbstractPerUserSystemService<CredentialManagerServiceImpl, CredentialManagerService> {
     private static final String TAG = "CredManSysServiceImpl";
 
@@ -34,6 +39,20 @@
         super(master, lock, userId);
     }
 
+    @Override // from PerUserSystemService
+    protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
+            throws PackageManager.NameNotFoundException {
+        ServiceInfo si;
+        try {
+            si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
+                    PackageManager.GET_META_DATA, mUserId);
+        } catch (RemoteException e) {
+            throw new PackageManager.NameNotFoundException(
+                    "Could not get service for " + serviceComponent);
+        }
+        return si;
+    }
+
     /**
      * Unimplemented getCredentials
      */
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9e449ae..b74fedf 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1792,7 +1792,8 @@
                 t.traceBegin("StartStatusBarManagerService");
                 try {
                     statusBar = new StatusBarManagerService(context);
-                    ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar);
+                    ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar, false,
+                            DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO);
                 } catch (Throwable e) {
                     reportWtf("starting StatusBarManagerService", e);
                 }
diff --git a/services/tests/mockingservicestests/OWNERS b/services/tests/mockingservicestests/OWNERS
index 2bb1649..4dda51f 100644
--- a/services/tests/mockingservicestests/OWNERS
+++ b/services/tests/mockingservicestests/OWNERS
@@ -1,5 +1,8 @@
 include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS
+
+# Game Platform
 per-file FakeGameClassifier.java = file:/GAME_MANAGER_OWNERS
 per-file FakeGameServiceProviderInstance = file:/GAME_MANAGER_OWNERS
 per-file FakeServiceConnector.java = file:/GAME_MANAGER_OWNERS
 per-file Game* = file:/GAME_MANAGER_OWNERS
+per-file res/xml/game_manager* = file:/GAME_MANAGER_OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 90b1f4e..b7e66f2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -39,6 +39,8 @@
 import android.app.BroadcastOptions;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -289,20 +291,30 @@
         final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
                 PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
 
+        // enqueue a bg-priority broadcast then a fg-priority one
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone);
+        queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, 0);
+
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
         queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, 0);
 
+        // verify that:
+        // (a) the queue is immediately runnable by existence of a fg-priority broadcast
+        // (b) the next one up is the fg-priority broadcast despite its later enqueue time
         queue.setProcessCached(false);
         assertTrue(queue.isRunnable());
         assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
         assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
+        assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord);
 
         queue.setProcessCached(true);
         assertTrue(queue.isRunnable());
         assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
         assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
+        assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord);
     }
 
     /**
@@ -386,4 +398,86 @@
         assertEquals(Intent.ACTION_SCREEN_OFF, queue.getActive().intent.getAction());
         assertTrue(queue.isEmpty());
     }
+
+    /**
+     * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MOST_RECENT works as expected.
+     */
+    @Test
+    public void testDeliveryGroupPolicy_mostRecent() {
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastOptions optionsTimeTick = BroadcastOptions.makeBasic();
+        optionsTimeTick.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+
+        final Intent musicVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
+        musicVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+                AudioManager.STREAM_MUSIC);
+        final BroadcastOptions optionsMusicVolumeChanged = BroadcastOptions.makeBasic();
+        optionsMusicVolumeChanged.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+        optionsMusicVolumeChanged.setDeliveryGroupKey("audio",
+                String.valueOf(AudioManager.STREAM_MUSIC));
+
+        final Intent alarmVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
+        alarmVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+                AudioManager.STREAM_ALARM);
+        final BroadcastOptions optionsAlarmVolumeChanged = BroadcastOptions.makeBasic();
+        optionsAlarmVolumeChanged.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+        optionsAlarmVolumeChanged.setDeliveryGroupKey("audio",
+                String.valueOf(AudioManager.STREAM_ALARM));
+
+        // Halt all processing so that we get a consistent view
+        mHandlerThread.getLooper().getQueue().postSyncBarrier();
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+
+        final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        // Verify that the older musicVolumeChanged has been removed.
+        verifyPendingRecords(queue,
+                List.of(timeTick, alarmVolumeChanged, musicVolumeChanged));
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        // Verify that the older alarmVolumeChanged has been removed.
+        verifyPendingRecords(queue,
+                List.of(timeTick, musicVolumeChanged, alarmVolumeChanged));
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        // Verify that the older timeTick has been removed.
+        verifyPendingRecords(queue,
+                List.of(musicVolumeChanged, alarmVolumeChanged, timeTick));
+    }
+
+    private void verifyPendingRecords(BroadcastProcessQueue queue,
+            List<Intent> intents) {
+        for (int i = 0; i < intents.size(); i++) {
+            queue.makeActiveNextPending();
+            final Intent actualIntent = queue.getActive().intent;
+            final Intent expectedIntent = intents.get(i);
+            final String errMsg = "actual=" + actualIntent + ", expected=" + expectedIntent
+                    + ", actual_extras=" + actualIntent.getExtras()
+                    + ", expected_extras=" + expectedIntent.getExtras();
+            assertTrue(errMsg, actualIntent.filterEquals(expectedIntent));
+            assertTrue(errMsg, Bundle.kindofEquals(
+                    actualIntent.getExtras(), expectedIntent.getExtras()));
+        }
+        assertTrue(queue.isEmpty());
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index c125448..d9a26c6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -549,12 +549,6 @@
                 receivers, false, null, null, userId);
     }
 
-    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp,
-            List<Object> receivers, IIntentReceiver orderedResultTo, Bundle orderedExtras) {
-        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
-                receivers, true, orderedResultTo, orderedExtras, UserHandle.USER_SYSTEM);
-    }
-
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             BroadcastOptions options, List<Object> receivers) {
         return makeBroadcastRecord(intent, callerApp, options,
@@ -562,12 +556,24 @@
     }
 
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
+            List<Object> receivers, IIntentReceiver resultTo) {
+        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
+                receivers, false, resultTo, null, UserHandle.USER_SYSTEM);
+    }
+
+    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp,
+            List<Object> receivers, IIntentReceiver resultTo, Bundle resultExtras) {
+        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
+                receivers, true, resultTo, resultExtras, UserHandle.USER_SYSTEM);
+    }
+
+    private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             BroadcastOptions options, List<Object> receivers, boolean ordered,
-            IIntentReceiver orderedResultTo, Bundle orderedExtras, int userId) {
+            IIntentReceiver resultTo, Bundle resultExtras, int userId) {
         return new BroadcastRecord(mQueue, intent, callerApp, callerApp.info.packageName, null,
                 callerApp.getPid(), callerApp.info.uid, false, null, null, null, null,
-                AppOpsManager.OP_NONE, options, receivers, callerApp, orderedResultTo,
-                Activity.RESULT_OK, null, orderedExtras, ordered, false, false, userId, false, null,
+                AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo,
+                Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId, false, null,
                 false, null);
     }
 
@@ -1347,6 +1353,26 @@
     }
 
     /**
+     * Verify that we deliver results for unordered broadcasts.
+     */
+    @Test
+    public void testUnordered_ResultTo() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final IApplicationThread callerThread = callerApp.getThread();
+
+        final IIntentReceiver resultTo = mock(IIntentReceiver.class);
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)), resultTo));
+
+        waitForIdle();
+        verify(callerThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)),
+                eq(Activity.RESULT_OK), any(), any(), eq(false),
+                anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt());
+    }
+
+    /**
      * Verify that we're not surprised by a process attempting to finishing a
      * broadcast when none is in progress.
      */
diff --git a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
index 581a2a7..2d7d46f 100644
--- a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.fail;
 
 import android.app.backup.BackupTransport;
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
 import android.app.backup.RestoreSet;
 import android.content.Intent;
@@ -254,6 +255,9 @@
             ITransportStatusCallback c) throws RemoteException {}
         @Override public void abortFullRestore(ITransportStatusCallback c) throws RemoteException {}
         @Override public void getTransportFlags(AndroidFuture<Integer> f) throws RemoteException {}
+        @Override
+        public void getBackupManagerMonitor(AndroidFuture<IBackupManagerMonitor> resultFuture)
+                throws RemoteException {}
         @Override public IBinder asBinder() {
             return null;
         }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index eb131419..ffacbf3 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.biometrics.sensors;
 
-import static android.testing.TestableLooper.RunWithLooper;
+import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
 
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
@@ -24,8 +24,10 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -35,6 +37,7 @@
 import static org.mockito.Mockito.withSettings;
 
 import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.IBiometricService;
 import android.os.Binder;
@@ -63,27 +66,25 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.function.Supplier;
 
 @Presubmit
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class BiometricSchedulerTest {
 
     private static final String TAG = "BiometricSchedulerTest";
     private static final int TEST_SENSOR_ID = 1;
     private static final int LOG_NUM_RECENT_OPERATIONS = 2;
-
-    private BiometricScheduler mScheduler;
-    private IBinder mToken;
-
-    @Mock
-    private IBiometricService mBiometricService;
-
     @Rule
     public final TestableContext mContext =
             new TestableContext(InstrumentationRegistry.getContext(), null);
+    private BiometricScheduler mScheduler;
+    private IBinder mToken;
+    @Mock
+    private IBiometricService mBiometricService;
 
     @Before
     public void setUp() {
@@ -323,7 +324,7 @@
         client1.getCallback().onClientFinished(client1, true /* success */);
         waitForIdle();
         verify(callback).onError(anyInt(), anyInt(),
-                eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
+                eq(BIOMETRIC_ERROR_CANCELED),
                 eq(0) /* vendorCode */);
         assertNull(mScheduler.getCurrentClient());
         assertTrue(client1.isAlreadyDone());
@@ -484,7 +485,7 @@
         mScheduler.scheduleClientMonitor(interrupter);
         waitForIdle();
 
-        verify((Interruptable) interruptableMonitor).cancel();
+        verify(interruptableMonitor).cancel();
         mScheduler.getInternalCallback().onClientFinished(interruptableMonitor, true /* success */);
     }
 
@@ -500,7 +501,7 @@
         mScheduler.scheduleClientMonitor(interrupter);
         waitForIdle();
 
-        verify((Interruptable) interruptableMonitor, never()).cancel();
+        verify(interruptableMonitor, never()).cancel();
     }
 
     @Test
@@ -514,21 +515,180 @@
         assertTrue(client.mDestroyed);
     }
 
+    @Test
+    public void testClearBiometricQueue_clearsHungAuthOperation() {
+        // Creating a hung client
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+        waitForIdle();
+
+        mScheduler.startWatchdog();
+        waitForIdle();
+
+        //Checking client is hung
+        verify(callback1).onClientStarted(client1);
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+        assertNotNull(mScheduler.mCurrentOperation);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        // The hung client did not honor this operation, verify onError and authenticated
+        // were never called.
+        assertFalse(client1.mOnErrorCalled);
+        assertFalse(client1.mAuthenticateCalled);
+        verify(callback1).onClientFinished(client1, false /* success */);
+        assertNull(mScheduler.mCurrentOperation);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+    }
+
+    @Test
+    public void testAuthWorks_afterClearBiometricQueue() {
+        // Creating a hung client
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+
+        assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        //Checking client is hung
+        waitForIdle();
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+
+        // The watchdog should kick off the cancellation
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        // The hung client did not honor this operation, verify onError and authenticated
+        // were never called.
+        assertFalse(client1.mOnErrorCalled);
+        assertFalse(client1.mAuthenticateCalled);
+        verify(callback1).onClientFinished(client1, false /* success */);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+        assertNull(mScheduler.mCurrentOperation);
+
+
+        //Run additional auth client
+        final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
+        final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client2, callback2);
+
+        assertEquals(client2, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        waitForIdle();
+
+        //Ensure auth client passes
+        verify(callback2).onClientStarted(client2);
+        client2.getCallback().onClientFinished(client2, true);
+        waitForIdle();
+
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        //Asserting auth client passes
+        assertTrue(client2.isAlreadyDone());
+        assertNotNull(mScheduler.mCurrentOperation);
+    }
+
+    @Test
+    public void testClearBiometricQueue_doesNotClearOperationsWhenQueueNotStuck() {
+        //Creating clients
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        waitForIdle();
+
+        assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(2, mScheduler.getCurrentPendingCount());
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+        verify(callback1).onClientStarted(client1);
+
+        //Client finishes successfully
+        client1.getCallback().onClientFinished(client1, true);
+        waitForIdle();
+
+        // The watchdog should kick off the cancellation
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        //Watchdog does not clear pending operations
+        assertEquals(1, mScheduler.getCurrentPendingCount());
+        assertNotNull(mScheduler.mCurrentOperation);
+
+    }
+
     private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception {
         return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer));
     }
 
+    private void waitForIdle() {
+        TestableLooper.get(this).processAllMessages();
+    }
+
     private static class TestAuthenticationClient extends AuthenticationClient<Object> {
         boolean mStartedHal = false;
         boolean mStoppedHal = false;
         boolean mDestroyed = false;
         int mNumCancels = 0;
+        boolean mAuthenticateCalled = false;
+        boolean mOnErrorCalled = false;
 
-        public TestAuthenticationClient(@NonNull Context context,
+        TestAuthenticationClient(@NonNull Context context,
                 @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
                 @NonNull ClientMonitorCallbackConverter listener) {
+            this(context, lazyDaemon, token, listener, 1 /* cookie */);
+        }
+
+        TestAuthenticationClient(@NonNull Context context,
+                @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
+                @NonNull ClientMonitorCallbackConverter listener, int cookie) {
             super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */,
-                    false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */,
+                    false /* restricted */, TAG, cookie, false /* requireConfirmation */,
                     TEST_SENSOR_ID, mock(BiometricLogger.class), mock(BiometricContext.class),
                     true /* isStrongBiometric */, null /* taskStackListener */,
                     mock(LockoutTracker.class), false /* isKeyguard */,
@@ -546,7 +706,19 @@
         }
 
         @Override
-        protected void handleLifecycleAfterAuth(boolean authenticated) {}
+        protected void handleLifecycleAfterAuth(boolean authenticated) {
+        }
+
+        @Override
+        public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
+                boolean authenticated, ArrayList<Byte> hardwareAuthToken) {
+            mAuthenticateCalled = true;
+        }
+
+        @Override
+        protected void onErrorInternal(int errorCode, int vendorCode, boolean finish) {
+            mOnErrorCalled = true;
+        }
 
         @Override
         public boolean wasUserDetected() {
@@ -651,8 +823,4 @@
             mDestroyed = true;
         }
     }
-
-    private void waitForIdle() {
-        TestableLooper.get(this).processAllMessages();
-    }
 }
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 73548a3..1b5db0a 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
@@ -41,6 +41,7 @@
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
@@ -74,6 +75,7 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
@@ -131,6 +133,8 @@
     private Probe mLuxProbe;
     @Mock
     private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private Clock mClock;
     @Captor
     private ArgumentCaptor<OperationContext> mOperationContextCaptor;
     @Captor
@@ -451,6 +455,52 @@
     }
 
     @Test
+    public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        final int vendorAcquireMessage = 1234;
+
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
+                FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
+                vendorAcquireMessage);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        mLooper.dispatchAll();
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage);
+        mLooper.dispatchAll();
+
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
+    @Test
+    public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        final int vendorAcquireMessage = 1234;
+
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
+                FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
+                vendorAcquireMessage);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        mLooper.dispatchAll();
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1);
+        mLooper.dispatchAll();
+
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+    }
+
+    @Test
     public void sideFingerprintSendsAuthIfFingerUp() throws Exception {
         when(mSensorProps.isAnySidefpsType()).thenReturn(true);
 
@@ -497,6 +547,79 @@
         verify(mCallback).onClientFinished(any(), eq(true));
     }
 
+    @Test
+    public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception {
+        final int powerWindow = 500;
+        final long authStart = 300;
+
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+
+        // Acquire start occurs at time = 0ms
+        when(mClock.millis()).thenReturn(0L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // Auth occurs at time = 300
+        when(mClock.millis()).thenReturn(authStart);
+        // At this point the delay should be 500 - (300 - 0) == 200 milliseconds.
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        // After waiting 200 milliseconds, auth should succeed.
+        mLooper.moveTimeForward(powerWindow - authStart);
+        mLooper.dispatchAll();
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
+    @Test
+    public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception {
+        final int powerWindow = 500;
+
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        // Acquire start occurs at time = 0ms
+        when(mClock.millis()).thenReturn(0L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // Auth reject occurs at time = 300ms
+        when(mClock.millis()).thenReturn(300L);
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                false /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+
+        mLooper.moveTimeForward(300);
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        when(mClock.millis()).thenReturn(1300L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // If code is correct, the new acquired start timestamp should be used
+        // and the code should only have to wait 500 - (1500-1300)ms.
+        when(mClock.millis()).thenReturn(1500L);
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+
+        mLooper.moveTimeForward(299);
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        mLooper.moveTimeForward(1);
+        mLooper.dispatchAll();
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
     private FingerprintAuthenticationClient createClient() throws RemoteException {
         return createClient(100 /* version */, true /* allowBackgroundAuthentication */);
     }
@@ -524,7 +647,7 @@
                 null /* taskStackListener */, mLockoutCache,
                 mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication,
                 mSensorProps,
-                new Handler(mLooper.getLooper()), 0 /* biometricStrength */) {
+                new Handler(mLooper.getLooper()), 0 /* biometricStrength */, mClock) {
             @Override
             protected ActivityTaskManager getActivityTaskManager() {
                 return mActivityTaskManager;
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index 6b8c26d..d2f2af1 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.companion.virtual;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -25,6 +27,7 @@
 
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.IInputManager;
+import android.hardware.input.InputManager;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -88,6 +91,30 @@
     }
 
     @Test
+    public void registerInputDevice_deviceCreation_hasDeviceId() {
+        final IBinder device1Token = new Binder("device1");
+        mInputController.createMouse("mouse", /*vendorId= */ 1, /*productId= */ 1, device1Token,
+                /* displayId= */ 1);
+        int device1Id = mInputController.getInputDeviceId(device1Token);
+
+        final IBinder device2Token = new Binder("device2");
+        mInputController.createKeyboard("keyboard", /*vendorId= */2, /*productId= */ 2,
+                device2Token, 2);
+        int device2Id = mInputController.getInputDeviceId(device2Token);
+
+        assertWithMessage("Different devices should have different id").that(
+                device1Id).isNotEqualTo(device2Id);
+
+
+        int[] deviceIds = InputManager.getInstance().getInputDeviceIds();
+        assertWithMessage("InputManager's deviceIds list should contain id of device 1").that(
+                deviceIds).asList().contains(device1Id);
+        assertWithMessage("InputManager's deviceIds list should contain id of device 2").that(
+                deviceIds).asList().contains(device2Id);
+
+    }
+
+    @Test
     public void unregisterInputDevice_allMiceUnregistered_clearPointerDisplayId() {
         final IBinder deviceToken = new Binder();
         mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
@@ -115,4 +142,5 @@
         mInputController.unregisterInputDevice(deviceToken);
         verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
     }
+
 }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 9c5d1a5..02bbe65 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -121,6 +121,7 @@
     private static final int VENDOR_ID = 5;
     private static final String UNIQUE_ID = "uniqueid";
     private static final String PHYS = "phys";
+    private static final int DEVICE_ID = 42;
     private static final int HEIGHT = 1800;
     private static final int WIDTH = 900;
     private static final Binder BINDER = new Binder("binder");
@@ -530,6 +531,16 @@
     }
 
     @Test
+    public void createVirtualKeyboard_inputDeviceId_obtainFromInputController() {
+        final int fd = 1;
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */ 1, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
+        assertWithMessage(
+                "InputController should return device id from InputDeviceDescriptor").that(
+                mInputController.getInputDeviceId(BINDER)).isEqualTo(DEVICE_ID);
+    }
+
+    @Test
     public void onAudioSessionStarting_hasVirtualAudioController() {
         mDeviceImpl.onVirtualDisplayCreatedLocked(
                 mDeviceImpl.createWindowPolicyController(), DISPLAY_ID);
@@ -576,9 +587,9 @@
         final int fd = 1;
         final int keyCode = KeyEvent.KEYCODE_A;
         final int action = VirtualKeyEvent.ACTION_UP;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 1,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */1, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
+
         mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder().setKeyCode(keyCode)
                 .setAction(action).build());
         verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action);
@@ -601,9 +612,8 @@
         final int fd = 1;
         final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
         final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder()
                 .setButtonCode(buttonCode)
@@ -616,9 +626,8 @@
         final int fd = 1;
         final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
         final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -642,9 +651,8 @@
         final int fd = 1;
         final float x = -0.2f;
         final float y = 0.7f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder()
                 .setRelativeX(x).setRelativeY(y).build());
@@ -656,9 +664,8 @@
         final int fd = 1;
         final float x = -0.2f;
         final float y = 0.7f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -683,9 +690,8 @@
         final int fd = 1;
         final float x = 0.5f;
         final float y = 1f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder()
                 .setXAxisMovement(x)
@@ -698,9 +704,8 @@
         final int fd = 1;
         final float x = 0.5f;
         final float y = 1f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -731,9 +736,8 @@
         final float x = 100.5f;
         final float y = 200.5f;
         final int action = VirtualTouchEvent.ACTION_UP;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x)
                 .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType).build());
         verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN,
@@ -750,9 +754,8 @@
         final int action = VirtualTouchEvent.ACTION_UP;
         final float pressure = 1.0f;
         final float majorAxisSize = 10.0f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x)
                 .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType)
                 .setPressure(pressure).setMajorAxisSize(majorAxisSize).build());
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 6860abf..062bde8 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 
 import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX;
 
@@ -660,6 +661,117 @@
                 firstDisplayId);
     }
 
+    /** Tests that the virtual device is created in a device display group. */
+    @Test
+    public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
+                .thenReturn(true);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+
+        // Create a first virtual display. A display group should be created for this display on the
+        // virtual device.
+        final VirtualDisplayConfig.Builder builder1 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId1 =
+                localService.createVirtualDisplay(
+                        builder1.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+
+        // Create a second virtual display. This should be added to the previously created display
+        // group.
+        final VirtualDisplayConfig.Builder builder2 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId2 =
+                localService.createVirtualDisplay(
+                        builder2.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+
+        assertEquals(
+                "Both displays should be added to the same displayGroup.",
+                displayGroupId1,
+                displayGroupId2);
+    }
+
+    /**
+     * Tests that the virtual display is not added to the device display group when
+     * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set.
+     */
+    @Test
+    public void createVirtualDisplay_addsDisplaysToOwnDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
+                .thenReturn(true);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+
+        // Create a first virtual display. A display group should be created for this display on the
+        // virtual device.
+        final VirtualDisplayConfig.Builder builder1 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId1 =
+                localService.createVirtualDisplay(
+                        builder1.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+
+        // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
+        // the display should not be added to the previously created display group.
+        final VirtualDisplayConfig.Builder builder2 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId2 =
+                localService.createVirtualDisplay(
+                        builder2.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+
+        assertNotEquals(
+                "Display 1 should be in the device display group and display 2 in its own display"
+                        + " group.",
+                displayGroupId1,
+                displayGroupId2);
+    }
+
     @Test
     public void testGetDisplayIdToMirror() throws Exception {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 0b33c30..657bda6 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -369,6 +369,98 @@
     }
 
     @Test
+    public void testDevicesAreAddedToDeviceDisplayGroups() {
+        // Create the default internal display of the device.
+        LogicalDisplay defaultDisplay =
+                add(
+                        createDisplayDevice(
+                                Display.TYPE_INTERNAL,
+                                600,
+                                800,
+                                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
+
+        // Create 3 virtual displays associated with a first virtual device.
+        int deviceId1 = 1;
+        TestDisplayDevice display1 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display1", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display1, deviceId1);
+        LogicalDisplay virtualDevice1Display1 = add(display1);
+
+        TestDisplayDevice display2 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display2", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display2, deviceId1);
+        LogicalDisplay virtualDevice1Display2 = add(display2);
+
+        TestDisplayDevice display3 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display3", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display3, deviceId1);
+        LogicalDisplay virtualDevice1Display3 = add(display3);
+
+        // Create another 3 virtual displays associated with a second virtual device.
+        int deviceId2 = 2;
+        TestDisplayDevice display4 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display1", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display4, deviceId2);
+        LogicalDisplay virtualDevice2Display1 = add(display4);
+
+        TestDisplayDevice display5 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display2", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display5, deviceId2);
+        LogicalDisplay virtualDevice2Display2 = add(display5);
+
+        // The final display is created with FLAG_OWN_DISPLAY_GROUP set.
+        TestDisplayDevice display6 =
+                createDisplayDevice(
+                        Display.TYPE_VIRTUAL,
+                        "virtualDevice2Display3",
+                        600,
+                        800,
+                        DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display6, deviceId2);
+        LogicalDisplay virtualDevice2Display3 = add(display6);
+
+        // Verify that the internal display is in the default display group.
+        assertEquals(
+                DEFAULT_DISPLAY_GROUP,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(defaultDisplay)));
+
+        // Verify that all the displays for virtual device 1 are in the same (non-default) display
+        // group.
+        int virtualDevice1DisplayGroupId =
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display1));
+        assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice1DisplayGroupId);
+        assertEquals(
+                virtualDevice1DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display2)));
+        assertEquals(
+                virtualDevice1DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display3)));
+
+        // The first 2 displays for virtual device 2 should be in the same non-default group.
+        int virtualDevice2DisplayGroupId =
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display1));
+        assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice2DisplayGroupId);
+        assertEquals(
+                virtualDevice2DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display2)));
+        // virtualDevice2Display3 was created with FLAG_OWN_DISPLAY_GROUP and shouldn't be grouped
+        // with other displays of this device or be in the default display group.
+        assertNotEquals(
+                virtualDevice2DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display3)));
+        assertNotEquals(
+                DEFAULT_DISPLAY_GROUP,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display3)));
+    }
+
+    @Test
     public void testDeviceShouldBeWoken() {
         assertTrue(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN,
                 DEVICE_STATE_CLOSED,
@@ -416,14 +508,22 @@
     /////////////////
 
     private TestDisplayDevice createDisplayDevice(int type, int width, int height, int flags) {
-        return createDisplayDevice(new TestUtils.TestDisplayAddress(), type, width, height, flags);
+        return createDisplayDevice(
+                new TestUtils.TestDisplayAddress(), /*  uniqueId */ "", type, width, height, flags);
     }
 
     private TestDisplayDevice createDisplayDevice(
-            DisplayAddress address, int type, int width, int height, int flags) {
+            int type, String uniqueId, int width, int height, int flags) {
+        return createDisplayDevice(
+                new TestUtils.TestDisplayAddress(), uniqueId, type, width, height, flags);
+    }
+
+    private TestDisplayDevice createDisplayDevice(
+            DisplayAddress address, String uniqueId, int type, int width, int height, int flags) {
         TestDisplayDevice device = new TestDisplayDevice();
         DisplayDeviceInfo displayDeviceInfo = device.getSourceInfo();
         displayDeviceInfo.type = type;
+        displayDeviceInfo.uniqueId = uniqueId;
         displayDeviceInfo.width = width;
         displayDeviceInfo.height = height;
         displayDeviceInfo.flags = flags;
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
new file mode 100644
index 0000000..303a370
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.dreams;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.service.dreams.IDreamService;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamControllerTest {
+    @Mock
+    private DreamController.Listener mListener;
+    @Mock
+    private Context mContext;
+    @Mock
+    private IBinder mIBinder;
+    @Mock
+    private IDreamService mIDreamService;
+
+    @Captor
+    private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor;
+    @Captor
+    private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor;
+
+    private final TestLooper mLooper = new TestLooper();
+    private final Handler mHandler = new Handler(mLooper.getLooper());
+
+    private DreamController mDreamController;
+
+    private Binder mToken;
+    private ComponentName mDreamName;
+    private ComponentName mOverlayName;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mIDreamService.asBinder()).thenReturn(mIBinder);
+        when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService);
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+
+        mToken = new Binder();
+        mDreamName = ComponentName.unflattenFromString("dream");
+        mOverlayName = ComponentName.unflattenFromString("dream_overlay");
+        mDreamController = new DreamController(mContext, mHandler, mListener);
+    }
+
+    @Test
+    public void startDream_attachOnServiceConnected() throws RemoteException {
+        // Call dream controller to start dreaming.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+
+        // Mock service connected.
+        final ServiceConnection serviceConnection = captureServiceConnection();
+        serviceConnection.onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+
+        // Verify that dream service is called to attach.
+        verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any());
+    }
+
+    @Test
+    public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted()
+            throws RemoteException {
+        // Start first dream.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+        clearInvocations(mContext);
+
+        // Set up second dream.
+        final Binder newToken = new Binder();
+        final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream");
+        final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay");
+        final IDreamService newDreamService = mock(IDreamService.class);
+        final IBinder newBinder = mock(IBinder.class);
+        when(newDreamService.asBinder()).thenReturn(newBinder);
+        when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService);
+
+        // Start second dream.
+        mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(newDreamName, newBinder);
+        mLooper.dispatchAll();
+
+        // Mock second dream started.
+        verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/,
+                mRemoteCallbackCaptor.capture());
+        mRemoteCallbackCaptor.getValue().sendResult(null /*data*/);
+        mLooper.dispatchAll();
+
+        // Verify that the first dream is called to detach.
+        verify(mIDreamService).detach();
+    }
+
+    @Test
+    public void stopDream_detachFromService() throws RemoteException {
+        // Start dream.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+
+        // Stop dream.
+        mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/);
+
+        // Verify that dream service is called to detach.
+        verify(mIDreamService).detach();
+    }
+
+    private ServiceConnection captureServiceConnection() {
+        verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(),
+                any());
+        return mServiceConnectionACaptor.getValue();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
index 65076a3..b095a50 100644
--- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
@@ -32,6 +32,7 @@
 import android.platform.test.annotations.Presubmit
 import android.view.InputDevice
 import androidx.test.InstrumentationRegistry
+import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS
 import com.android.server.input.BatteryController.UEventManager
 import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener
 import org.hamcrest.Description
@@ -42,6 +43,8 @@
 import org.hamcrest.core.IsEqual.equalTo
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
@@ -63,14 +66,20 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.verification.VerificationMode
 
-private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice =
+private fun createInputDevice(
+    deviceId: Int,
+    hasBattery: Boolean = true,
+    supportsUsi: Boolean = false,
+    generation: Int = -1,
+): InputDevice =
     InputDevice.Builder()
         .setId(deviceId)
         .setName("Device $deviceId")
         .setDescriptor("descriptor $deviceId")
         .setExternal(true)
         .setHasBattery(hasBattery)
-        .setGeneration(0)
+        .setSupportsUsi(supportsUsi)
+        .setGeneration(generation)
         .build()
 
 // Returns a matcher that helps match member variables of a class.
@@ -118,7 +127,10 @@
     return Matchers.allOf(batteryStateMatchers)
 }
 
-// Helper used to verify interactions with a mocked battery listener.
+private fun isInvalidBatteryState(deviceId: Int): Matcher<IInputDeviceBatteryState> =
+    matchesState(deviceId, isPresent = false, status = STATUS_UNKNOWN, capacity = Float.NaN)
+
+// Helpers used to verify interactions with a mocked battery listener.
 private fun IInputDeviceBatteryListener.verifyNotified(
     deviceId: Int,
     mode: VerificationMode = times(1),
@@ -127,8 +139,21 @@
     capacity: Float? = null,
     eventTime: Long? = null
 ) {
-    verify(this, mode).onBatteryStateChanged(
-        MockitoHamcrest.argThat(matchesState(deviceId, isPresent, status, capacity, eventTime)))
+    verifyNotified(matchesState(deviceId, isPresent, status, capacity, eventTime), mode)
+}
+
+private fun IInputDeviceBatteryListener.verifyNotified(
+    matcher: Matcher<IInputDeviceBatteryState>,
+    mode: VerificationMode = times(1)
+) {
+    verify(this, mode).onBatteryStateChanged(MockitoHamcrest.argThat(matcher))
+}
+
+private fun createMockListener(): IInputDeviceBatteryListener {
+    val listener = mock(IInputDeviceBatteryListener::class.java)
+    val binder = mock(Binder::class.java)
+    `when`(listener.asBinder()).thenReturn(binder)
+    return listener
 }
 
 /**
@@ -143,6 +168,8 @@
         const val PID = 42
         const val DEVICE_ID = 13
         const val SECOND_DEVICE_ID = 11
+        const val USI_DEVICE_ID = 101
+        const val SECOND_USI_DEVICE_ID = 102
         const val TIMESTAMP = 123456789L
     }
 
@@ -168,10 +195,11 @@
         testLooper = TestLooper()
         val inputManager = InputManager.resetInstance(iInputManager)
         `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
-        `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID))
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(createInputDevice(DEVICE_ID))
-        `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID))
-            .thenReturn(createInputDevice(SECOND_DEVICE_ID))
+        `when`(iInputManager.inputDeviceIds).then {
+            deviceGenerationMap.keys.toIntArray()
+        }
+        addInputDevice(DEVICE_ID)
+        addInputDevice(SECOND_DEVICE_ID)
 
         batteryController = BatteryController(context, native, testLooper.looper, uEventManager)
         batteryController.systemRunning()
@@ -180,10 +208,30 @@
         devicesChangedListener = listenerCaptor.value
     }
 
-    private fun notifyDeviceChanged(deviceId: Int) {
-        deviceGenerationMap[deviceId] = deviceGenerationMap[deviceId]?.plus(1) ?: 1
+    private fun notifyDeviceChanged(
+            deviceId: Int,
+        hasBattery: Boolean = true,
+        supportsUsi: Boolean = false
+    ) {
+        val generation = deviceGenerationMap[deviceId]?.plus(1)
+            ?: throw IllegalArgumentException("Device $deviceId was never added!")
+        deviceGenerationMap[deviceId] = generation
+
+        `when`(iInputManager.getInputDevice(deviceId))
+            .thenReturn(createInputDevice(deviceId, hasBattery, supportsUsi, generation))
         val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) }
-        devicesChangedListener.onInputDevicesChanged(list.toIntArray())
+        if (::devicesChangedListener.isInitialized) {
+            devicesChangedListener.onInputDevicesChanged(list.toIntArray())
+        }
+    }
+
+    private fun addInputDevice(
+            deviceId: Int,
+        hasBattery: Boolean = true,
+        supportsUsi: Boolean = false
+    ) {
+        deviceGenerationMap[deviceId] = 0
+        notifyDeviceChanged(deviceId, hasBattery, supportsUsi)
     }
 
     @After
@@ -191,13 +239,6 @@
         InputManager.clearInstance()
     }
 
-    private fun createMockListener(): IInputDeviceBatteryListener {
-        val listener = mock(IInputDeviceBatteryListener::class.java)
-        val binder = mock(Binder::class.java)
-        `when`(listener.asBinder()).thenReturn(binder)
-        return listener
-    }
-
     @Test
     fun testRegisterAndUnregisterBinderLifecycle() {
         val listener = createMockListener()
@@ -303,19 +344,14 @@
         listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f)
 
         // If the battery presence for the InputDevice changes, the listener is notified.
-        `when`(iInputManager.getInputDevice(DEVICE_ID))
-            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = false))
-        notifyDeviceChanged(DEVICE_ID)
+        notifyDeviceChanged(DEVICE_ID, hasBattery = false)
         testLooper.dispatchNext()
-        listener.verifyNotified(DEVICE_ID, isPresent = false, status = STATUS_UNKNOWN,
-            capacity = Float.NaN)
+        listener.verifyNotified(isInvalidBatteryState(DEVICE_ID))
         // Since the battery is no longer present, the UEventListener should be removed.
         verify(uEventManager).removeListener(uEventListener.value)
 
         // If the battery becomes present again, the listener is notified.
-        `when`(iInputManager.getInputDevice(DEVICE_ID))
-            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = true))
-        notifyDeviceChanged(DEVICE_ID)
+        notifyDeviceChanged(DEVICE_ID, hasBattery = true)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING,
             capacity = 0.78f)
@@ -340,9 +376,17 @@
 
         // Move the time forward so that the polling period has elapsed.
         // The listener should be notified.
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS - 1)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS - 1)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, capacity = 0.80f)
+
+        // Move the time forward so that another polling period has elapsed.
+        // The battery should still be polled, but there is no change so listeners are not notified.
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
+        testLooper.dispatchNext()
+        listener.verifyNotified(DEVICE_ID, mode = times(1), capacity = 0.80f)
     }
 
     @Test
@@ -357,7 +401,8 @@
         // The battery state changed, but we should not be polling for battery changes when the
         // device is not interactive.
         `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80)
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchAll()
         listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f)
 
@@ -368,7 +413,8 @@
 
         // Ensure that we continue to poll for battery changes.
         `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90)
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, capacity = 0.90f)
     }
@@ -398,4 +444,44 @@
             matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f))
         listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)
     }
+
+    @Test
+    fun testUsiDeviceIsMonitoredPersistently() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+
+        // Even though there is no listener added for this device, it is being monitored.
+        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+        verify(uEventManager)
+            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))
+
+        // Add and remove a listener for the device.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+        batteryController.unregisterBatteryListener(USI_DEVICE_ID, listener, PID)
+
+        // The device is still being monitored.
+        verify(uEventManager, never()).removeListener(uEventListener.value)
+    }
+
+    @Test
+    fun testNoPollingWhenUsiDevicesAreMonitored() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+        `when`(native.getBatteryDevicePath(SECOND_USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device2")
+        addInputDevice(SECOND_USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
+
+        // Add a listener.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 3bcde6a..b7f90d4 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -113,8 +113,10 @@
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
@@ -134,6 +136,7 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.net.wifi.WifiInfo;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.INetworkManagementService;
 import android.os.PersistableBundle;
@@ -152,6 +155,7 @@
 import android.test.suitebuilder.annotation.MediumTest;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.DataUnit;
 import android.util.Log;
 import android.util.Pair;
@@ -171,11 +175,12 @@
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.usage.AppStandbyInternal;
 
-import libcore.io.Streams;
-
 import com.google.common.util.concurrent.AbstractFuture;
 
+import libcore.io.Streams;
+
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -286,6 +291,8 @@
     private NetworkPolicyListenerAnswer mPolicyListener;
     private NetworkPolicyManagerService mService;
 
+    private final ArraySet<BroadcastReceiver> mRegisteredReceivers = new ArraySet<>();
+
     /**
      * In some of the tests while initializing NetworkPolicyManagerService,
      * ACTION_RESTRICT_BACKGROUND_CHANGED is broadcasted. This is for capturing that broadcast.
@@ -437,6 +444,21 @@
             public void enforceCallingOrSelfPermission(String permission, String message) {
                 // Assume that we're AID_SYSTEM
             }
+
+            @Override
+            public Intent registerReceiver(BroadcastReceiver receiver,
+                    IntentFilter filter, String broadcastPermission, Handler scheduler) {
+                mRegisteredReceivers.add(receiver);
+                return super.registerReceiver(receiver, filter, broadcastPermission, scheduler);
+            }
+
+            @Override
+            public Intent registerReceiverForAllUsers(BroadcastReceiver receiver,
+                    IntentFilter filter, String broadcastPermission, Handler scheduler) {
+                mRegisteredReceivers.add(receiver);
+                return super.registerReceiverForAllUsers(receiver, filter, broadcastPermission,
+                        scheduler);
+            }
         };
 
         setNetpolicyXml(context);
@@ -557,6 +579,13 @@
         RecurrenceRule.sClock = Clock.systemDefaultZone();
     }
 
+    @After
+    public void unregisterReceivers() throws Exception {
+        for (BroadcastReceiver receiver : mRegisteredReceivers) {
+            mServiceContext.unregisterReceiver(receiver);
+        }
+    }
+
     @Test
     public void testTurnRestrictBackgroundOn() throws Exception {
         assertRestrictBackgroundOff();
@@ -2033,6 +2062,9 @@
 
     @Test
     public void testNormalizeTemplate_duplicatedMergedImsiList() {
+        // This test leads to a Log.wtf, so skip it on eng builds. Otherwise, Log.wtf() would
+        // result in this process getting killed.
+        Assume.assumeFalse(Build.IS_ENG);
         final NetworkTemplate template = new NetworkTemplate.Builder(MATCH_CARRIER)
                 .setSubscriberIds(Set.of(TEST_IMSI)).build();
         final String[] mergedImsiGroup = new String[] {TEST_IMSI, TEST_IMSI};
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
index 94e67d1..3f55f1b 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
@@ -16,16 +16,16 @@
 
 package com.android.server.om;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -43,6 +43,9 @@
     private static final String OVERLAY2 = OVERLAY + "2";
     private static final OverlayIdentifier IDENTIFIER2 = new OverlayIdentifier(OVERLAY2);
 
+    @Rule
+    public final Expect expect = Expect.create();
+
     @Test
     public void alwaysInitializeAllPackages() {
         final OverlayManagerServiceImpl impl = getImpl();
@@ -51,13 +54,11 @@
         addPackage(target(otherTarget), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER),
-                        new PackageAndUser(otherTarget, USER),
-                        new PackageAndUser(OVERLAY, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        // The result should be the same for every time
+        assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
+        assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
     }
 
     @Test
@@ -66,29 +67,31 @@
         addPackage(target(TARGET), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertFalse(o1.isEnabled());
-        assertFalse(o1.isMutable);
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.isEnabled()).isFalse();
+        expect.that(o1.isMutable).isFalse();
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o2);
-        assertTrue(o2.isEnabled());
-        assertFalse(o2.isMutable);
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.isEnabled()).isTrue();
+        expect.that(o2.isMutable).isFalse();
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertFalse(o3.isEnabled());
-        assertFalse(o3.isMutable);
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.isEnabled()).isFalse();
+        expect.that(o3.isMutable).isFalse();
     }
 
     @Test
@@ -98,28 +101,30 @@
         addPackage(overlay(OVERLAY, TARGET), USER);
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertFalse(o1.isEnabled());
-        assertTrue(o1.isMutable);
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.isEnabled()).isFalse();
+        expect.that(o1.isMutable).isTrue();
 
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o2);
-        assertFalse(o2.isEnabled());
-        assertTrue(o2.isMutable);
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.isEnabled()).isFalse();
+        expect.that(o2.isMutable).isTrue();
 
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertFalse(o3.isEnabled());
-        assertTrue(o3.isMutable);
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.isEnabled()).isFalse();
+        expect.that(o3.isMutable).isTrue();
     }
 
     @Test
@@ -128,17 +133,17 @@
         addPackage(target(TARGET), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
         final Consumer<ConfigState> setOverlay = (state -> {
             configureSystemOverlay(OVERLAY, state, 0 /* priority */);
-            assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+            expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
             final OverlayInfo o = impl.getOverlayInfo(IDENTIFIER, USER);
-            assertNotNull(o);
-            assertEquals(o.isEnabled(), state == ConfigState.IMMUTABLE_ENABLED
+            expect.that(o).isNotNull();
+            assertThat(expect.hasFailures()).isFalse();
+            expect.that(o.isEnabled()).isEqualTo(state == ConfigState.IMMUTABLE_ENABLED
                     || state == ConfigState.MUTABLE_ENABLED);
-            assertEquals(o.isMutable, state == ConfigState.MUTABLE_DISABLED
+            expect.that(o.isMutable).isEqualTo(state == ConfigState.MUTABLE_DISABLED
                     || state == ConfigState.MUTABLE_ENABLED);
         });
 
@@ -180,20 +185,20 @@
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 1 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER),
-                        new PackageAndUser(OVERLAY2, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertEquals(0, o1.priority);
-        assertFalse(o1.isEnabled());
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.priority).isEqualTo(0);
+        expect.that(o1.isEnabled()).isFalse();
 
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o2);
-        assertEquals(1, o2.priority);
-        assertFalse(o2.isEnabled());
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.priority).isEqualTo(1);
+        expect.that(o2.isEnabled()).isFalse();
 
         // Overlay priority changing between reboots should not affect enable state of mutable
         // overlays.
@@ -202,16 +207,18 @@
         // Reorder the overlays
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 1 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertEquals(1, o3.priority);
-        assertTrue(o3.isEnabled());
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.priority).isEqualTo(1);
+        expect.that(o3.isEnabled()).isTrue();
 
         final OverlayInfo o4 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o4);
-        assertEquals(0, o4.priority);
-        assertFalse(o4.isEnabled());
+        expect.that(o4).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o4.priority).isEqualTo(0);
+        expect.that(o4.isEnabled()).isFalse();
     }
 
     @Test
@@ -223,33 +230,35 @@
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER),
-                        new PackageAndUser(OVERLAY2, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertEquals(0, o1.priority);
-        assertTrue(o1.isEnabled());
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.priority).isEqualTo(0);
+        expect.that(o1.isEnabled()).isTrue();
 
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o2);
-        assertEquals(1, o2.priority);
-        assertTrue(o2.isEnabled());
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.priority).isEqualTo(1);
+        expect.that(o2.isEnabled()).isTrue();
 
         // Reorder the overlays
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertEquals(1, o3.priority);
-        assertTrue(o3.isEnabled());
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.priority).isEqualTo(1);
+        expect.that(o3.isEnabled()).isTrue();
 
         final OverlayInfo o4 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o4);
-        assertEquals(0, o4.priority);
-        assertTrue(o4.isEnabled());
+        expect.that(o4).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o4.priority).isEqualTo(0);
+        expect.that(o4.isEnabled()).isTrue();
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 96707fd..00aa520 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -176,6 +176,13 @@
     }
 
     @Test
+    public void testHasUserRestriction_NonExistentUserReturnsFalse() {
+        int nonExistentUserId = UserHandle.USER_NULL;
+        assertThat(mUserManagerService.hasUserRestriction(DISALLOW_USER_SWITCH, nonExistentUserId))
+                .isFalse();
+    }
+
+    @Test
     public void testSetUserRestrictionWithIncorrectID() throws Exception {
         int incorrectId = 1;
         while (mUserManagerService.userExists(incorrectId)) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
index d5893c8..77d542a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
@@ -52,7 +52,6 @@
         assertFalse(opt.isBootComplete());
         assertFalse(opt.isCheckForProfileUpdates());
         assertFalse(opt.isDexoptOnlySecondaryDex());
-        assertFalse(opt.isDexoptOnlySharedDex());
         assertFalse(opt.isDowngrade());
         assertFalse(opt.isForce());
         assertFalse(opt.isDexoptIdleBackgroundJob());
@@ -67,7 +66,6 @@
                 DexoptOptions.DEXOPT_BOOT_COMPLETE |
                 DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES |
                 DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX |
-                DexoptOptions.DEXOPT_ONLY_SHARED_DEX |
                 DexoptOptions.DEXOPT_DOWNGRADE  |
                 DexoptOptions.DEXOPT_AS_SHARED_LIBRARY |
                 DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB |
@@ -81,7 +79,6 @@
         assertTrue(opt.isBootComplete());
         assertTrue(opt.isCheckForProfileUpdates());
         assertTrue(opt.isDexoptOnlySecondaryDex());
-        assertTrue(opt.isDexoptOnlySharedDex());
         assertTrue(opt.isDowngrade());
         assertTrue(opt.isForce());
         assertTrue(opt.isDexoptAsSharedLibrary());
@@ -113,7 +110,6 @@
             assertTrue(opt.isBootComplete());
             assertTrue(opt.isCheckForProfileUpdates());
             assertFalse(opt.isDexoptOnlySecondaryDex());
-            assertFalse(opt.isDexoptOnlySharedDex());
             assertFalse(opt.isDowngrade());
             assertTrue(opt.isForce());
             assertFalse(opt.isDexoptAsSharedLibrary());
@@ -131,7 +127,6 @@
         assertTrue(opt.isBootComplete());
         assertFalse(opt.isCheckForProfileUpdates());
         assertFalse(opt.isDexoptOnlySecondaryDex());
-        assertFalse(opt.isDexoptOnlySharedDex());
         assertFalse(opt.isDowngrade());
         assertTrue(opt.isForce());
         assertFalse(opt.isDexoptAsSharedLibrary());
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index fe4db3a..db2630e2 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -87,7 +87,6 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
-import com.android.server.compat.PlatformCompat;
 import com.android.server.lights.LightsManager;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.power.PowerManagerService.BatteryReceiver;
@@ -147,7 +146,6 @@
     @Mock private SystemPropertiesWrapper mSystemPropertiesMock;
     @Mock private AppOpsManager mAppOpsManagerMock;
     @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock;
-    @Mock private PlatformCompat mPlatformCompat;
 
     @Mock
     private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
@@ -321,11 +319,6 @@
             AppOpsManager createAppOpsManager(Context context) {
                 return mAppOpsManagerMock;
             }
-
-            @Override
-            PlatformCompat createPlatformCompat(Context context) {
-                return mPlatformCompat;
-            }
         });
         return mService;
     }
@@ -505,9 +498,6 @@
         String packageName = "pkg.name";
         when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON,
                 Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED);
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(true);
         when(mContextSpy.checkCallingOrSelfPermission(
                 android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
                 PackageManager.PERMISSION_GRANTED);
@@ -532,23 +522,6 @@
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
         assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */);
-
-        // Verify that on older platforms only the appOp is necessary and the permission isn't
-        // checked
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(false);
-        when(mContextSpy.checkCallingOrSelfPermission(
-                android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
-                PackageManager.PERMISSION_DENIED);
-        forceSleep();
-        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
-
-        flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
-        mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
-                null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
-        mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */);
     }
 
     @Test
@@ -568,7 +541,7 @@
         int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
         mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         } else {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
@@ -577,9 +550,6 @@
 
         when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON,
                 Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED);
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(true);
         when(mContextSpy.checkCallingOrSelfPermission(
                 android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
                 PackageManager.PERMISSION_DENIED);
@@ -589,7 +559,7 @@
         flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
         mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         } else {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
index 235849c..c484f45 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -53,7 +53,8 @@
 
     private boolean mIsAvailable = true;
     private boolean mIsInfoLoadSuccessful = true;
-    private long mLatency;
+    private long mOnLatency;
+    private long mOffLatency;
     private int mOffCount;
 
     private int mCapabilities;
@@ -97,7 +98,7 @@
         public long on(long milliseconds, long vibrationId) {
             recordEffectSegment(vibrationId, new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE,
                     /* frequencyHz= */ 0, (int) milliseconds));
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(milliseconds, vibrationId);
             return milliseconds;
         }
@@ -105,12 +106,13 @@
         @Override
         public void off() {
             mOffCount++;
+            applyLatency(mOffLatency);
         }
 
         @Override
         public void setAmplitude(float amplitude) {
             mAmplitudes.add(amplitude);
-            applyLatency();
+            applyLatency(mOnLatency);
         }
 
         @Override
@@ -121,7 +123,7 @@
             }
             recordEffectSegment(vibrationId,
                     new PrebakedSegment((int) effect, false, (int) strength));
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(EFFECT_DURATION, vibrationId);
             return EFFECT_DURATION;
         }
@@ -141,7 +143,7 @@
                 duration += EFFECT_DURATION + primitive.getDelay();
                 recordEffectSegment(vibrationId, primitive);
             }
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(duration, vibrationId);
             return duration;
         }
@@ -154,7 +156,7 @@
                 recordEffectSegment(vibrationId, primitive);
             }
             recordBraking(vibrationId, braking);
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(duration, vibrationId);
             return duration;
         }
@@ -193,10 +195,10 @@
             return mIsInfoLoadSuccessful;
         }
 
-        private void applyLatency() {
+        private void applyLatency(long latencyMillis) {
             try {
-                if (mLatency > 0) {
-                    Thread.sleep(mLatency);
+                if (latencyMillis > 0) {
+                    Thread.sleep(latencyMillis);
                 }
             } catch (InterruptedException e) {
             }
@@ -240,10 +242,15 @@
 
     /**
      * Sets the latency this controller should fake for turning the vibrator hardware on or setting
-     * it's vibration amplitude.
+     * the vibration amplitude.
      */
-    public void setLatency(long millis) {
-        mLatency = millis;
+    public void setOnLatency(long millis) {
+        mOnLatency = millis;
+    }
+
+    /** Sets the latency this controller should fake for turning the vibrator off. */
+    public void setOffLatency(long millis) {
+        mOffLatency = millis;
     }
 
     /** Set the capabilities of the fake vibrator hardware. */
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
index a15e4b0..fc830a9 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -1159,7 +1159,7 @@
 
         // 25% of the first waveform step will be spent on the native on() call.
         // 25% of each waveform step will be spent on the native setAmplitude() call..
-        mVibratorProviders.get(VIBRATOR_ID).setLatency(stepDuration / 4);
+        mVibratorProviders.get(VIBRATOR_ID).setOnLatency(stepDuration / 4);
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         int stepCount = totalDuration / stepDuration;
@@ -1190,7 +1190,7 @@
         fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
 
         long latency = 5_000; // 5s
-        fakeVibrator.setLatency(latency);
+        fakeVibrator.setOnLatency(latency);
 
         long vibrationId = 1;
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
@@ -1204,8 +1204,7 @@
         // fail at waitForCompletion(cancellingThread).
         Thread cancellingThread = new Thread(
                 () -> conductor.notifyCancelled(
-                        new Vibration.EndInfo(
-                                Vibration.Status.CANCELLED_BY_USER),
+                        new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER),
                         /* immediate= */ false));
         cancellingThread.start();
 
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index c46fecd..c83afb7 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -826,13 +826,40 @@
         // The second vibration shouldn't have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
         // No segment played is the prebaked CLICK from the second vibration.
-        assertFalse(
-                mVibratorProviders.get(1).getAllEffectSegments().stream()
-                        .anyMatch(segment -> segment instanceof PrebakedSegment));
+        assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
         cancelVibrate(service);  // Clean up repeating effect.
     }
 
     @Test
+    public void vibrate_withOngoingRepeatingVibrationBeingCancelled_playsAfterPreviousIsCancelled()
+            throws Exception {
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setOffLatency(50); // Add latency so cancellation is slow.
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        VibratorManagerService service = createSystemReadyService();
+
+        VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
+                new long[]{10, 10_000}, new int[]{255, 0}, 1);
+        vibrate(service, repeatingEffect, ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async, wait until the off waveform step.
+        assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS));
+
+        // Cancel vibration right before requesting a new one.
+        // This should trigger slow IVibrator.off before setting the vibration status to cancelled.
+        cancelVibrate(service);
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                ALARM_ATTRS);
+
+        // Check that second vibration was played.
+        assertTrue(fakeVibrator.getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
+    }
+
+    @Test
     public void vibrate_withNewRepeatingVibration_cancelsOngoingEffect() throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -880,10 +907,8 @@
         // The second vibration shouldn't have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
         // The second vibration shouldn't have played any prebaked segment.
-        assertFalse(
-                mVibratorProviders.get(1).getAllEffectSegments().stream()
-                        .anyMatch(segment -> segment instanceof PrebakedSegment));
-
+        assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
         cancelVibrate(service);  // Clean up long effect.
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 91c2fe0..8e81e2d 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -1371,6 +1371,39 @@
         verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
     }
 
+    @Test
+    public void dreamWhenDocked_ambientModeSuppressed_suppressionEnabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
+    @Test
+    public void dreamWhenDocked_ambientModeSuppressed_suppressionDisabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(false);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
+    @Test
+    public void dreamWhenDocked_ambientModeNotSuppressed_suppressionEnabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(false);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
     private void triggerDockIntent() {
         final Intent dockedIntent =
                 new Intent(Intent.ACTION_DOCK_EVENT)
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 1e94577..248a3fc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.notification;
 
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
@@ -30,9 +31,11 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.intThat;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -49,6 +52,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.content.pm.VersionedPackage;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerFilter;
@@ -69,6 +73,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.internal.util.reflection.FieldSetter;
@@ -77,6 +82,7 @@
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
 import java.util.List;
 
 public class NotificationListenersTest extends UiServiceTestCase {
@@ -85,6 +91,8 @@
     private PackageManager mPm;
     @Mock
     private IPackageManager miPm;
+    @Mock
+    private Resources mResources;
 
     @Mock
     NotificationManagerService mNm;
@@ -96,7 +104,8 @@
 
     private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp");
     private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2");
-
+    private ComponentName mUninstalledComponent = new ComponentName("pkg3",
+            "pkg3.NotificationListenerService");
 
     @Before
     public void setUp() throws Exception {
@@ -111,7 +120,7 @@
 
     @Test
     public void testReadExtraTag() throws Exception {
-        String xml = "<" + TAG_REQUESTED_LISTENERS+ ">"
+        String xml = "<" + TAG_REQUESTED_LISTENERS + ">"
                 + "<listener component=\"" + mCn1.flattenToString() + "\" user=\"0\">"
                 + "<allowed types=\"7\" />"
                 + "</listener>"
@@ -131,11 +140,55 @@
     }
 
     @Test
+    public void loadDefaultsFromConfig_forHeadlessSystemUser_loadUninstalled() throws Exception {
+        // setup with headless system user mode
+        mListeners = spy(mNm.new NotificationListeners(
+                mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm,
+                /* isHeadlessSystemUserMode= */ true));
+        mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent);
+
+        mListeners.loadDefaultsFromConfig();
+
+        assertThat(mListeners.getDefaultComponents()).contains(mUninstalledComponent);
+    }
+
+    @Test
+    public void loadDefaultsFromConfig_forNonHeadlessSystemUser_ignoreUninstalled()
+            throws Exception {
+        // setup without headless system user mode
+        mListeners = spy(mNm.new NotificationListeners(
+                mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm,
+                /* isHeadlessSystemUserMode= */ false));
+        mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent);
+
+        mListeners.loadDefaultsFromConfig();
+
+        assertThat(mListeners.getDefaultComponents()).doesNotContain(mUninstalledComponent);
+    }
+
+    private void mockDefaultListenerConfigForUninstalledComponent(ComponentName componentName) {
+        ArraySet<ComponentName> components = new ArraySet<>(Arrays.asList(componentName));
+        when(mResources
+                .getString(
+                        com.android.internal.R.string.config_defaultListenerAccessPackages))
+                .thenReturn(componentName.getPackageName());
+        when(mContext.getResources()).thenReturn(mResources);
+        doReturn(components).when(mListeners).queryPackageForServices(
+                eq(componentName.getPackageName()),
+                intThat(hasIntBitFlag(MATCH_ANY_USER)),
+                anyInt());
+    }
+
+    public static ArgumentMatcher<Integer> hasIntBitFlag(int flag) {
+        return arg -> arg != null && ((arg & flag) == flag);
+    }
+
+    @Test
     public void testWriteExtraTag() throws Exception {
         NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
         VersionedPackage a1 = new VersionedPackage("pkg1", 243);
         NotificationListenerFilter nlf2 =
-                new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
+                new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[]{a1}));
         mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
         mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
deleted file mode 100644
index d765042..0000000
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
+++ /dev/null
@@ -1,551 +0,0 @@
-/*
- * Copyright (C) 2017 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.assertNotNull;
-import static junit.framework.Assert.assertNull;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Person;
-import android.app.RemoteInput;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.style.StyleSpan;
-import android.util.Pair;
-import android.widget.RemoteViews;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.UiServiceTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class NotificationTest extends UiServiceTestCase {
-
-    @Mock
-    ActivityManager mAm;
-
-    @Mock
-    Resources mResources;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    public void testDoesNotStripsExtenders() {
-        Notification.Builder nb = new Notification.Builder(mContext, "channel");
-        nb.extend(new Notification.CarExtender().setColor(Color.RED));
-        nb.extend(new Notification.TvExtender().setChannelId("different channel"));
-        nb.extend(new Notification.WearableExtender().setDismissalId("dismiss"));
-        Notification before = nb.build();
-        Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before);
-
-        assertTrue(before == after);
-
-        assertEquals("different channel", new Notification.TvExtender(before).getChannelId());
-        assertEquals(Color.RED, new Notification.CarExtender(before).getColor());
-        assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId());
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_noStyles() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test");
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-
-        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_noStyleToStyle() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test");
-        Notification.Builder n2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_styleToNoStyle() {
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_changeStyle() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle());
-        Notification.Builder n2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testInboxTextChange() {
-        Notification.Builder nInbox1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle().addLine("a").addLine("b"));
-        Notification.Builder nInbox2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle().addLine("b").addLine("c"));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2));
-    }
-
-    @Test
-    public void testBigTextTextChange() {
-        Notification.Builder nBigText1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle().bigText("something"));
-        Notification.Builder nBigText2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle().bigText("else"));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2));
-    }
-
-    @Test
-    public void testBigPictureChange() {
-        Bitmap bitA = mock(Bitmap.class);
-        when(bitA.getGenerationId()).thenReturn(100);
-        Bitmap bitB = mock(Bitmap.class);
-        when(bitB.getGenerationId()).thenReturn(200);
-
-        Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigPictureStyle().bigPicture(bitA));
-        Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigPictureStyle().bigPicture(bitB));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2));
-    }
-
-    @Test
-    public void testMessagingChange_text() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class)))
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "b", 100, mock(Person.class)))
-                );
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_data() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))
-                                .setData("text", mock(Uri.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_sender() {
-        Person a = mock(Person.class);
-        when(a.getName()).thenReturn("A");
-        Person b = mock(Person.class);
-        when(b.getName()).thenReturn("b");
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_key() {
-        Person a = mock(Person.class);
-        when(a.getKey()).thenReturn("A");
-        Person b = mock(Person.class);
-        when(b.getKey()).thenReturn("b");
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_ignoreTimeChange() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 1000, mock(Person.class)))
-                );
-
-        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testRemoteViews_nullChange() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setContent(mock(RemoteViews.class));
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test")
-                .setContent(mock(RemoteViews.class));
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test")
-                .setCustomBigContentView(mock(RemoteViews.class));
-        n2 = new Notification.Builder(mContext, "test");
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test")
-                .setCustomBigContentView(mock(RemoteViews.class));
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test");
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_layoutChange() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(189);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_layoutSame() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_sequenceChange() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        when(a.getSequenceNumber()).thenReturn(1);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-        when(b.getSequenceNumber()).thenReturn(2);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_sequenceSame() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        when(a.getSequenceNumber()).thenReturn(1);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-        when(b.getSequenceNumber()).thenReturn(1);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferent_null() {
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentSame() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentText() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
-                .build();
-
-        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentSpannables() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon,
-                        new SpannableStringBuilder().append("test1",
-                                new StyleSpan(Typeface.BOLD),
-                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE),
-                        intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "test1", intent).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentNumber() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
-                .build();
-
-        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentIntent() {
-        PendingIntent intent1 = mock(PendingIntent.class);
-        PendingIntent intent2 = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsIgnoresRemoteInputs() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(new RemoteInput.Builder("a")
-                                .setChoices(new CharSequence[] {"i", "m"})
-                                .build())
-                        .build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(new RemoteInput.Builder("a")
-                                .setChoices(new CharSequence[] {"t", "m"})
-                                .build())
-                        .build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_noRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .build())
-                .build();
-        assertNull(notification.findRemoteInputActionPair(false));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_hasRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        RemoteInput remoteInput = new RemoteInput.Builder("a").build();
-
-        Notification.Action actionWithRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(remoteInput)
-                        .addRemoteInput(remoteInput)
-                        .build();
-
-        Notification.Action actionWithoutRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 2", intent)
-                        .build();
-
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(actionWithoutRemoteInput)
-                .addAction(actionWithRemoteInput)
-                .build();
-
-        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
-                notification.findRemoteInputActionPair(false);
-
-        assertNotNull(remoteInputActionPair);
-        assertEquals(remoteInput, remoteInputActionPair.first);
-        assertEquals(actionWithRemoteInput, remoteInputActionPair.second);
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(
-                                new RemoteInput.Builder("a")
-                                        .setAllowFreeFormInput(false).build())
-                        .build())
-                .build();
-        assertNull(notification.findRemoteInputActionPair(true));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        RemoteInput remoteInput =
-                new RemoteInput.Builder("a").setAllowFreeFormInput(false).build();
-        RemoteInput freeformRemoteInput =
-                new RemoteInput.Builder("b").setAllowFreeFormInput(true).build();
-
-        Notification.Action actionWithFreeformRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(remoteInput)
-                        .addRemoteInput(freeformRemoteInput)
-                        .build();
-
-        Notification.Action actionWithoutFreeformRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 2", intent)
-                        .addRemoteInput(remoteInput)
-                        .build();
-
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(actionWithoutFreeformRemoteInput)
-                .addAction(actionWithFreeformRemoteInput)
-                .build();
-
-        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
-                notification.findRemoteInputActionPair(true);
-
-        assertNotNull(remoteInputActionPair);
-        assertEquals(freeformRemoteInput, remoteInputActionPair.first);
-        assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second);
-    }
-}
-
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index d5e336b..eed32d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -40,14 +40,18 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 
+import android.app.ActivityOptions;
 import android.app.WaitResult;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.os.Binder;
 import android.os.ConditionVariable;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.view.Display;
@@ -308,4 +312,40 @@
         waitHandlerIdle(mAtm.mH);
         verify(mRootWindowContainer, timeout(TIMEOUT_MS)).startHomeOnEmptyDisplays("userUnlocked");
     }
+
+    /** Verifies that launch from recents sets the launch cookie on the activity. */
+    @Test
+    public void testStartActivityFromRecents_withLaunchCookie() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        IBinder launchCookie = new Binder("test_launch_cookie");
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchCookie(launchCookie);
+        SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options.toBundle());
+
+        doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(),
+                anyInt(), any());
+
+        mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions);
+
+        assertThat(activity.mLaunchCookie).isEqualTo(launchCookie);
+        verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
+    }
+
+    /** Verifies that launch from recents doesn't set the launch cookie on the activity. */
+    @Test
+    public void testStartActivityFromRecents_withoutLaunchCookie() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(
+                ActivityOptions.makeBasic().toBundle());
+
+        doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(),
+                anyInt(), any());
+
+        mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions);
+
+        assertThat(activity.mLaunchCookie).isNull();
+        verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index c3d49e1..bc319db 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -242,7 +242,7 @@
     private IOnBackInvokedCallback createOnBackInvokedCallback() {
         return new IOnBackInvokedCallback.Stub() {
             @Override
-            public void onBackStarted() {
+            public void onBackStarted(BackEvent backEvent) {
             }
 
             @Override
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 11ae5d4..e69418b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2314,6 +2314,8 @@
         assertEquals(displayWidth, windowConfig.getBounds().width());
         assertEquals(displayHeight, windowConfig.getBounds().height());
         assertEquals(windowingMode, windowConfig.getWindowingMode());
+        assertEquals(Configuration.SCREENLAYOUT_SIZE_NORMAL,
+                config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK);
 
         // test misc display overrides
         assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
@@ -2355,6 +2357,8 @@
         assertEquals(displayWidth, windowConfig.getBounds().width());
         assertEquals(displayHeight, windowConfig.getBounds().height());
         assertEquals(windowingMode, windowConfig.getWindowingMode());
+        assertEquals(Configuration.SCREENLAYOUT_SIZE_LARGE, testDisplayContent
+                .getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK);
 
         // test misc display overrides
         assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
index fc3962b..cd4d65d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
@@ -26,8 +26,10 @@
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.hardware.HardwareBuffer;
-import android.view.Surface;
 import android.platform.test.annotations.Presubmit;
+import android.view.Surface;
+
+import com.android.internal.policy.TransitionAnimation;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,7 +54,8 @@
     public void blackLuma() {
         Bitmap swBitmap = createBitmap(0);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
+
         assertEquals(0, borderLuma, 0);
     }
 
@@ -60,7 +63,7 @@
     public void whiteLuma() {
         Bitmap swBitmap = createBitmap(1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
@@ -68,7 +71,7 @@
     public void unevenBitmapDimens() {
         Bitmap swBitmap = createBitmap(1, BITMAP_WIDTH + 1, BITMAP_HEIGHT + 1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
@@ -77,7 +80,7 @@
         Bitmap swBitmap = createBitmap(1);
         setBorderLuma(swBitmap, 0);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(0, borderLuma, 0);
     }
 
@@ -86,7 +89,7 @@
         Bitmap swBitmap = createBitmap(0);
         setBorderLuma(swBitmap, 1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
index 2ae328b..394d6e7 100644
--- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.usb.UsbConfiguration;
+import android.hardware.usb.UsbConstants;
 import android.hardware.usb.UsbDevice;
 import android.hardware.usb.UsbDeviceConnection;
 import android.hardware.usb.UsbEndpoint;
@@ -76,10 +77,10 @@
     // event schedulers for each input port of the physical device
     private MidiEventScheduler[] mEventSchedulers;
 
-    // Arbitrary number for timeout to not continue sending to
-    // an inactive device. This number tries to balances the number
-    // of cycles and not being permanently stuck.
-    private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 10;
+    // Timeout for sending a packet to a device.
+    // If bulkTransfer times out, retry sending the packet up to 20 times.
+    private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 50;
+    private static final int BULK_TRANSFER_NUMBER_OF_RETRIES = 20;
 
     // Arbitrary number for timeout when closing a thread
     private static final int THREAD_JOIN_TIMEOUT_MILLISECONDS = 200;
@@ -386,10 +387,15 @@
                                     break;
                                 }
                                 final UsbRequest response = connectionFinal.requestWait();
-                                if (response != request) {
-                                    Log.w(TAG, "Unexpected response");
+                                if (response == null) {
+                                    Log.w(TAG, "Response is null");
                                     break;
                                 }
+                                if (request != response) {
+                                    Log.w(TAG, "Skipping response");
+                                    continue;
+                                }
+
                                 int bytesRead = byteBuffer.position();
 
                                 if (bytesRead > 0) {
@@ -513,9 +519,47 @@
                                             convertedArray.length);
                                 }
 
-                                connectionFinal.bulkTransfer(endpointFinal, convertedArray,
-                                        convertedArray.length,
-                                        BULK_TRANSFER_TIMEOUT_MILLISECONDS);
+                                boolean isInterrupted = false;
+                                // Split the packet into multiple if they are greater than the
+                                // endpoint's max packet size.
+                                for (int curPacketStart = 0;
+                                        curPacketStart < convertedArray.length &&
+                                        isInterrupted == false;
+                                        curPacketStart += endpointFinal.getMaxPacketSize()) {
+                                    int transferResult = -1;
+                                    int retryCount = 0;
+                                    int curPacketSize = Math.min(endpointFinal.getMaxPacketSize(),
+                                            convertedArray.length - curPacketStart);
+
+                                    // Keep trying to send the packet until the result is
+                                    // successful or until the retry limit is reached.
+                                    while (transferResult < 0 && retryCount <=
+                                            BULK_TRANSFER_NUMBER_OF_RETRIES) {
+                                        transferResult = connectionFinal.bulkTransfer(
+                                                endpointFinal,
+                                                convertedArray,
+                                                curPacketStart,
+                                                curPacketSize,
+                                                BULK_TRANSFER_TIMEOUT_MILLISECONDS);
+                                        retryCount++;
+
+                                        if (Thread.currentThread().interrupted()) {
+                                            Log.w(TAG, "output thread interrupted after send");
+                                            isInterrupted = true;
+                                            break;
+                                        }
+                                        if (transferResult < 0) {
+                                            Log.d(TAG, "retrying packet. retryCount = "
+                                                    + retryCount + " result = " + transferResult);
+                                            if (retryCount > BULK_TRANSFER_NUMBER_OF_RETRIES) {
+                                                Log.w(TAG, "Skipping packet because timeout");
+                                            }
+                                        }
+                                    }
+                                }
+                                if (isInterrupted == true) {
+                                    break;
+                                }
                                 eventSchedulerFinal.addEventToPool(event);
                             }
                         } catch (NullPointerException e) {
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index f43f0a5..d314a65 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -8658,11 +8658,12 @@
 
     /**
      * Boolean indicating if the VoNR setting is visible in the Call Settings menu.
-     * If true, the VoNR setting menu will be visible. If false, the menu will be gone.
+     * If this flag is set and VoNR is enabled for this carrier (see {@link #KEY_VONR_ENABLED_BOOL})
+     * the VoNR setting menu will be visible. If {@link #KEY_VONR_ENABLED_BOOL} or
+     * this setting is false, the menu will be gone.
      *
-     * Disabled by default.
+     * Enabled by default.
      *
-     * @hide
      */
     public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool";
 
@@ -8672,7 +8673,6 @@
      *
      * Disabled by default.
      *
-     * @hide
      */
     public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool";
 
@@ -8715,6 +8715,8 @@
      * premium capabilities should be blocked when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to user action or timeout.
+     * The maximum number of network boost notifications to show the user are defined in
+     * {@link #KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY}.
      *
      * The default value is 30 minutes.
      *
@@ -8726,6 +8728,22 @@
             "premium_capability_notification_backoff_hysteresis_time_millis_long";
 
     /**
+     * The maximum number of times that we display the notification for a network boost via premium
+     * capabilities when {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+     * returns a failure due to user action or timeout.
+     *
+     * An int array with 2 values: {max_notifications_per_day, max_notifications_per_month}.
+     *
+     * The default value is {2, 10}, meaning we display a maximum of 2 network boost notifications
+     * per day and 10 notifications per month.
+     *
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT
+     */
+    public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY =
+            "premium_capability_maximum_notification_count_int_array";
+
+    /**
      * The amount of time in milliseconds that the purchase request should be throttled when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to the carrier.
@@ -8752,6 +8770,20 @@
             "premium_capability_purchase_url_string";
 
     /**
+     * Whether to allow premium capabilities to be purchased when the device is connected to LTE.
+     * If this is {@code true}, applications can call
+     * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+     * when connected to {@link TelephonyManager#NETWORK_TYPE_LTE} to purchase and use
+     * premium capabilities.
+     * If this is {@code false}, applications can only purchase and use premium capabilities when
+     * conencted to {@link TelephonyManager#NETWORK_TYPE_NR}.
+     *
+     * This is {@code false} by default.
+     */
+    public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL =
+            "premium_capability_supported_on_lte_bool";
+
+    /**
      * IWLAN handover rules that determine whether handover is allowed or disallowed between
      * cellular and IWLAN.
      *
@@ -9432,15 +9464,18 @@
         sDefaults.putBoolean(KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL, false);
         sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, true);
         sDefaults.putBoolean(KEY_VONR_ENABLED_BOOL, false);
-        sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[]{});
+        sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[] {});
         sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
         sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
+        sDefaults.putIntArray(KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY,
+                new int[] {2, 10});
         sDefaults.putLong(
                 KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
         sDefaults.putString(KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, null);
+        sDefaults.putBoolean(KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL, false);
         sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{
                 "source=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, "
                         + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"});
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index eb3affc..439eaa6 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -54,6 +54,7 @@
 import android.os.ParcelUuid;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.provider.Telephony.SimInfo;
 import android.telephony.euicc.EuiccManager;
 import android.telephony.ims.ImsMmTelManager;
@@ -4154,4 +4155,79 @@
                 return "UNKNOWN(" + usageSetting + ")";
         }
     }
+
+    /**
+     * Set userHandle for a subscription.
+     *
+     * Used to set an association between a subscription and a user on the device so that voice
+     * calling and SMS from that subscription can be associated with that user.
+     * Data services are always shared between users on the device.
+     *
+     * @param subscriptionId the subId of the subscription.
+     * @param userHandle the userHandle associated with the subscription.
+     * Pass {@code null} user handle to clear the association.
+     *
+     * @throws IllegalArgumentException if subscription is invalid.
+     * @throws SecurityException if the caller doesn't have permissions required.
+     * @throws IllegalStateException if subscription service is not available.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+    public void setUserHandle(int subscriptionId, @Nullable UserHandle userHandle) {
+        if (!isValidSubscriptionId(subscriptionId)) {
+            throw new IllegalArgumentException("[setUserHandle]: Invalid subscriptionId: "
+                    + subscriptionId);
+        }
+
+        try {
+            ISub iSub = TelephonyManager.getSubscriptionService();
+            if (iSub != null) {
+                iSub.setUserHandle(userHandle, subscriptionId, mContext.getOpPackageName());
+            } else {
+                throw new IllegalStateException("[setUserHandle]: "
+                        + "subscription service unavailable");
+            }
+        } catch (RemoteException ex) {
+            ex.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Get UserHandle of this subscription.
+     *
+     * Used to get user handle associated with this subscription.
+     *
+     * @param subscriptionId the subId of the subscription.
+     * @return userHandle associated with this subscription
+     * or {@code null} if subscription is not associated with any user.
+     *
+     * @throws IllegalArgumentException if subscription is invalid.
+     * @throws SecurityException if the caller doesn't have permissions required.
+     * @throws IllegalStateException if subscription service is not available.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+    public @Nullable UserHandle getUserHandle(int subscriptionId) {
+        if (!isValidSubscriptionId(subscriptionId)) {
+            throw new IllegalArgumentException("[getUserHandle]: Invalid subscriptionId: "
+                    + subscriptionId);
+        }
+
+        try {
+            ISub iSub = TelephonyManager.getSubscriptionService();
+            if (iSub != null) {
+                return iSub.getUserHandle(subscriptionId, mContext.getOpPackageName());
+            } else {
+                throw new IllegalStateException("[getUserHandle]: "
+                        + "subscription service unavailable");
+            }
+        } catch (RemoteException ex) {
+            ex.rethrowAsRuntimeException();
+        }
+        return null;
+    }
 }
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index f3d48a8..97a464c 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -54,6 +54,7 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -17115,11 +17116,12 @@
     }
 
     /**
-     * A premium capability boosting the network to allow real-time interactive traffic.
-     * Corresponds to NetworkCapabilities#NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC.
+     * A premium capability that boosts the network to allow for real-time interactive traffic
+     * by prioritizing low latency communication.
+     * Corresponds to {@link NetworkCapabilities#NET_CAPABILITY_PRIORITIZE_LATENCY}.
      */
-    // TODO(b/245748544): add @link once NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC is defined.
-    public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1;
+    public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY =
+            NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
 
     /**
      * Purchasable premium capabilities.
@@ -17127,7 +17129,7 @@
      */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = { "PREMIUM_CAPABILITY_" }, value = {
-            PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC})
+            PREMIUM_CAPABILITY_PRIORITIZE_LATENCY})
     public @interface PremiumCapability {}
 
     /**
@@ -17139,8 +17141,8 @@
      */
     public static String convertPremiumCapabilityToString(@PremiumCapability int capability) {
         switch (capability) {
-            case PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC:
-                return "REALTIME_INTERACTIVE_TRAFFIC";
+            case PREMIUM_CAPABILITY_PRIORITIZE_LATENCY:
+                return "PRIORITIZE_LATENCY";
             default:
                 return "UNKNOWN (" + capability + ")";
         }
@@ -17178,11 +17180,18 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1;
 
     /**
-     * Purchase premium capability failed because the request is throttled for the amount of time
+     * Purchase premium capability failed because the request is throttled.
+     * If purchasing premium capabilities is throttled, it will be for the amount of time
      * specified by {@link CarrierConfigManager
-     * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}
-     * or {@link CarrierConfigManager
      * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}.
+     * If displaying the network boost notification is throttled, it will be for the amount of time
+     * specified by {@link CarrierConfigManager
+     * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_INT_ARRAY}.
+     * If a foreground application requests premium capabilities, the network boost notification
+     * will be displayed to the user regardless of the throttled status.
+     * We will show the network boost notification to the user up to the daily and monthly maximum
+     * number of times specified by {@link CarrierConfigManager
+     * #KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY}.
      * Subsequent attempts will return the same error until the request is no longer throttled
      * or throttling conditions change.
      */
@@ -17202,10 +17211,14 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4;
 
     /**
-     * Purchase premium capability failed because the user disabled the feature.
-     * Subsequent attempts will return the same error until the user re-enables the feature.
+     * Purchase premium capability failed because a foreground application requested the same
+     * capability. The notification for the current application will be dismissed and a new
+     * notification will be displayed to the user for the foreground application.
+     * Subsequent attempts will return
+     * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS} until the foreground
+     * application's request is completed.
      */
-    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5;
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5;
 
     /**
      * Purchase premium capability failed because the user canceled the operation.
@@ -17252,7 +17265,8 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10;
 
     /**
-     * Purchase premium capability failed because the telephony service is down or unavailable.
+     * Purchase premium capability failed because the telephony service is unavailable
+     * or there was an error in the phone process.
      * Subsequent attempts will return the same error until request conditions are satisfied.
      */
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11;
@@ -17274,6 +17288,14 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13;
 
     /**
+     * Purchase premium capability failed because the request was not made on the default data
+     * subscription, indicated by {@link SubscriptionManager#getDefaultDataSubscriptionId()}.
+     * Subsequent attempts will return the same error until the request is made on the default
+     * data subscription.
+     */
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14;
+
+    /**
      * Results of the purchase premium capability request.
      * @hide
      */
@@ -17283,14 +17305,15 @@
             PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS,
-            PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED,
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
-            PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED})
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED,
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA})
     public @interface PurchasePremiumCapabilityResult {}
 
     /**
@@ -17311,8 +17334,8 @@
                 return "ALREADY_PURCHASED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS:
                 return "ALREADY_IN_PROGRESS";
-            case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED:
-                return "USER_DISABLED";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN:
+                return "OVERRIDDEN";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED:
                 return "USER_CANCELED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED:
@@ -17329,6 +17352,8 @@
                 return "NETWORK_NOT_AVAILABLE";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED:
                 return "NETWORK_CONGESTED";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA:
+                return "NOT_DEFAULT_DATA";
             default:
                 return "UNKNOWN (" + result + ")";
         }
@@ -17346,7 +17371,7 @@
      * @param callback The result of the purchase request.
      *                 One of {@link PurchasePremiumCapabilityResult}.
      * @throws SecurityException if the caller does not hold permission READ_BASIC_PHONE_STATE.
-     * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid
+     * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid.
      */
     @RequiresPermission(android.Manifest.permission.READ_BASIC_PHONE_STATE)
     public void purchasePremiumCapability(@PremiumCapability int capability,
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 917f35b..0211a7f 100755
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -18,6 +18,7 @@
 
 import android.telephony.SubscriptionInfo;
 import android.os.ParcelUuid;
+import android.os.UserHandle;
 import com.android.internal.telephony.ISetOpportunisticDataCallback;
 
 interface ISub {
@@ -316,4 +317,28 @@
      * @throws SecurityException if doesn't have MODIFY_PHONE_STATE or Carrier Privileges
      */
     int setUsageSetting(int usageSetting, int subId, String callingPackage);
+
+     /**
+      * Set userHandle for this subscription.
+      *
+      * @param userHandle the user handle for this subscription
+      * @param subId the unique SubscriptionInfo index in database
+      * @param callingPackage The package making the IPC.
+      *
+      * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION
+      * @throws IllegalArgumentException if subId is invalid.
+      */
+    int setUserHandle(in UserHandle userHandle, int subId, String callingPackage);
+
+    /**
+     * Get UserHandle for this subscription
+     *
+     * @param subId the unique SubscriptionInfo index in database
+     * @param callingPackage the package making the IPC
+     * @return userHandle associated with this subscription.
+     *
+     * @throws SecurityException if doesn't have SMANAGE_SUBSCRIPTION_USER_ASSOCIATION
+     * @throws IllegalArgumentException if subId is invalid.
+     */
+     UserHandle getUserHandle(int subId, String callingPackage);
 }
diff --git a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
index 5a135c9..7fe4bae 100644
--- a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
+++ b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
@@ -16,7 +16,7 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.sample.rollbackapp" >
-    <uses-permission android:name="android.permission.TEST_MANAGE_ROLLBACKS" />
+    <uses-permission android:name="android.permission.MANAGE_ROLLBACKS" />
     <application
         android:label="@string/title_activity_main">
         <activity
@@ -28,4 +28,4 @@
             </intent-filter>
         </activity>
     </application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
index 916551a..79a2f1f 100644
--- a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
+++ b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
@@ -75,6 +75,7 @@
                         String rollbackStatus = "FAILED";
                         if (rollbackStatusCode == RollbackManager.STATUS_SUCCESS) {
                             rollbackStatus = "SUCCESS";
+                            mTriggerRollbackButton.setClickable(false);
                         }
                         makeToast("Status for rollback ID " + rollbackId + " is " + rollbackStatus);
                     }}, new IntentFilter(ACTION_NAME), Context.RECEIVER_NOT_EXPORTED);
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
index 68a450d..1b0f035 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -26,6 +26,7 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.android.tools.lint.detector.api.getUMethod
+import com.google.android.lint.aidl.hasPermissionMethodAnnotation
 import com.intellij.psi.PsiType
 import org.jetbrains.uast.UAnnotation
 import org.jetbrains.uast.UBlockExpression
@@ -149,11 +150,6 @@
             enabledByDefault = false
         )
 
-        private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
-            .any {
-                it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
-            }
-
         private fun isPermissionMethodReturnType(method: UMethod): Boolean =
             listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
 
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
index 5106111..d120e1d 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
@@ -18,37 +18,54 @@
 
 import com.android.tools.lint.detector.api.JavaContext
 import com.android.tools.lint.detector.api.Location
-import com.intellij.psi.PsiVariable
+import com.android.tools.lint.detector.api.getUMethod
+import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
 import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.ULiteralExpression
-import org.jetbrains.uast.UQualifiedReferenceExpression
-import org.jetbrains.uast.USimpleNameReferenceExpression
-import org.jetbrains.uast.asRecursiveLogString
+import org.jetbrains.uast.evaluateString
+import org.jetbrains.uast.visitor.AbstractUastVisitor
 
 /**
- * Helper ADT class that facilitates the creation of lint auto fixes
+ * Helper class that facilitates the creation of lint auto fixes
  *
  * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks
  * that should be migrated to @EnforcePermission(allOf={...})
  *
  * TODO: handle anyOf style annotations
  */
-sealed class EnforcePermissionFix {
-    abstract fun locations(): List<Location>
-    abstract fun javaAnnotationParameter(): String
-
-    fun javaAnnotation(): String = "@$ANNOTATION_ENFORCE_PERMISSION(${javaAnnotationParameter()})"
+data class EnforcePermissionFix(
+    val locations: List<Location>,
+    val permissionNames: List<String>
+) {
+    val annotation: String
+        get() {
+            val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" }
+            val annotationParameter =
+                if (permissionNames.size > 1) "allOf={$quotedPermissions}" else quotedPermissions
+            return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)"
+        }
 
     companion object {
-        fun fromCallExpression(callExpression: UCallExpression, context: JavaContext): SingleFix =
-            SingleFix(
-                getPermissionCheckLocation(context, callExpression),
-                getPermissionCheckArgumentValue(callExpression)
-            )
+        /**
+         * conditionally constructs EnforcePermissionFix from a UCallExpression
+         * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null
+         */
+        fun fromCallExpression(
+            context: JavaContext,
+            callExpression: UCallExpression
+        ): EnforcePermissionFix? =
+            if (isPermissionMethodCall(callExpression)) {
+                EnforcePermissionFix(
+                    listOf(getPermissionCheckLocation(context, callExpression)),
+                    getPermissionCheckValues(callExpression)
+                )
+            } else null
 
-        fun maybeAddManifestPrefix(permissionName: String): String =
-            if (permissionName.contains(".")) permissionName
-            else "android.Manifest.permission.$permissionName"
+
+        fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix =
+            EnforcePermissionFix(
+                individuals.flatMap { it.locations },
+                individuals.flatMap { it.permissionNames }
+            )
 
         /**
          * Given a permission check, get its proper location
@@ -70,49 +87,51 @@
         }
 
         /**
-         * Given a permission check and an argument,
-         * pull out the permission value that is being used
+         * Given a @PermissionMethod, find arguments annotated with @PermissionName
+         * and pull out the permission value(s) being used.  Also evaluates nested calls
+         * to @PermissionMethod(s) in the given method's body.
          */
-        private fun getPermissionCheckArgumentValue(
-            callExpression: UCallExpression,
-            argumentPosition: Int = 0
-        ): String {
+        private fun getPermissionCheckValues(
+            callExpression: UCallExpression
+        ): List<String> {
+            if (!isPermissionMethodCall(callExpression)) return emptyList()
 
-            val identifier = when (
-                val argument = callExpression.valueArguments.getOrNull(argumentPosition)
-            ) {
-                is UQualifiedReferenceExpression -> when (val selector = argument.selector) {
-                    is USimpleNameReferenceExpression ->
-                        ((selector.resolve() as PsiVariable).computeConstantValue() as String)
+            val result = mutableSetOf<String>() // protect against duplicate permission values
+            val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice
+            val bfsQueue = ArrayDeque(listOf(callExpression))
 
-                    else -> throw RuntimeException(
-                        "Couldn't resolve argument: ${selector.asRecursiveLogString()}"
-                    )
-                }
+            // Breadth First Search - evalutaing nested @PermissionMethod(s) in the available
+            // source code for @PermissionName(s).
+            while (bfsQueue.isNotEmpty()) {
+                val current = bfsQueue.removeFirst()
+                visitedCalls.add(current)
+                result.addAll(findPermissions(current))
 
-                is USimpleNameReferenceExpression -> (
-                        (argument.resolve() as PsiVariable).computeConstantValue() as String)
-
-                is ULiteralExpression -> argument.value as String
-
-                else -> throw RuntimeException(
-                    "Couldn't resolve argument: ${argument?.asRecursiveLogString()}"
-                )
+                current.resolve()?.getUMethod()?.accept(object : AbstractUastVisitor() {
+                    override fun visitCallExpression(node: UCallExpression): Boolean {
+                        if (isPermissionMethodCall(node) && node !in visitedCalls) {
+                            bfsQueue.add(node)
+                        }
+                        return false
+                    }
+                })
             }
 
-            return identifier.substringAfterLast(".")
+            return result.toList()
+        }
+
+        private fun findPermissions(
+            callExpression: UCallExpression,
+        ): List<String> {
+            val indices = callExpression.resolve()?.getUMethod()
+                ?.uastParameters
+                ?.filter(::hasPermissionNameAnnotation)
+                ?.mapNotNull { it.sourcePsi?.parameterIndex() }
+                ?: emptyList()
+
+            return indices.mapNotNull {
+                callExpression.getArgumentForParameter(it)?.evaluateString()
+            }
         }
     }
 }
-
-data class SingleFix(val location: Location, val permissionName: String) : EnforcePermissionFix() {
-    override fun locations(): List<Location> = listOf(this.location)
-    override fun javaAnnotationParameter(): String = maybeAddManifestPrefix(this.permissionName)
-}
-data class AllOfFix(val checks: List<SingleFix>) : EnforcePermissionFix() {
-    override fun locations(): List<Location> = this.checks.map { it.location }
-    override fun javaAnnotationParameter(): String =
-        "allOf={${
-            this.checks.joinToString(", ") { maybeAddManifestPrefix(it.permissionName) }
-        }}"
-}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
new file mode 100644
index 0000000..edbdd8d
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.getUMethod
+import com.google.android.lint.ANNOTATION_PERMISSION_METHOD
+import com.google.android.lint.ANNOTATION_PERMISSION_NAME
+import com.google.android.lint.CLASS_STUB
+import com.intellij.psi.PsiAnonymousClass
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+
+/**
+ * Given a UMethod, determine if this method is
+ * an entrypoint to an interface generated by AIDL,
+ * returning the interface name if so
+ */
+fun getContainingAidlInterface(node: UMethod): String? {
+    if (!isInClassCalledStub(node)) return null
+    for (superMethod in node.findSuperMethods()) {
+        for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
+            ?: continue) {
+            if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
+                return superMethod.containingClass?.name
+            }
+        }
+    }
+    return null
+}
+
+private fun isInClassCalledStub(node: UMethod): Boolean {
+    (node.containingClass as? PsiAnonymousClass)?.let {
+        return it.baseClassReference.referenceName == CLASS_STUB
+    }
+    return node.containingClass?.extendsList?.referenceElements?.any {
+        it.referenceName == CLASS_STUB
+    } ?: false
+}
+
+fun isPermissionMethodCall(callExpression: UCallExpression): Boolean {
+    val method = callExpression.resolve()?.getUMethod() ?: return false
+    return hasPermissionMethodAnnotation(method)
+}
+
+fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+    .any {
+        it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
+    }
+
+fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any {
+    it.hasQualifiedName(ANNOTATION_PERMISSION_NAME)
+}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
index 2cea394..2c53f39 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
@@ -25,9 +25,6 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.google.android.lint.CLASS_STUB
-import com.google.android.lint.ENFORCE_PERMISSION_METHODS
-import com.intellij.psi.PsiAnonymousClass
 import org.jetbrains.uast.UBlockExpression
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UElement
@@ -56,7 +53,7 @@
             val body = (node.uastBody as? UBlockExpression) ?: return
             val fix = accumulateSimplePermissionCheckFixes(body) ?: return
 
-            val javaRemoveFixes = fix.locations().map {
+            val javaRemoveFixes = fix.locations.map {
                 fix()
                     .replace()
                     .reformat(true)
@@ -67,7 +64,7 @@
             }
 
             val javaAnnotateFix = fix()
-                .annotate(fix.javaAnnotation())
+                .annotate(fix.annotation)
                 .range(context.getLocation(node))
                 .autoFix()
                 .build()
@@ -77,7 +74,7 @@
 
             context.report(
                 ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION,
-                fix.locations().last(),
+                fix.locations.last(),
                 message,
                 fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix)
             )
@@ -97,14 +94,14 @@
          */
         private fun accumulateSimplePermissionCheckFixes(methodBody: UBlockExpression):
                 EnforcePermissionFix? {
-            val singleFixes = mutableListOf<SingleFix>()
+            val singleFixes = mutableListOf<EnforcePermissionFix>()
             for (expression in methodBody.expressions) {
                 singleFixes.add(getPermissionCheckFix(expression) ?: break)
             }
             return when (singleFixes.size) {
                 0 -> null
                 1 -> singleFixes[0]
-                else -> AllOfFix(singleFixes)
+                else -> EnforcePermissionFix.compose(singleFixes)
             }
         }
 
@@ -113,7 +110,7 @@
          * the helper for creating a lint auto fix to @EnforcePermission
          */
         private fun getPermissionCheckFix(startingExpression: UElement?):
-                SingleFix? {
+                EnforcePermissionFix? {
             return when (startingExpression) {
                 is UQualifiedReferenceExpression -> getPermissionCheckFix(
                     startingExpression.selector
@@ -121,11 +118,8 @@
 
                 is UIfExpression -> getPermissionCheckFix(startingExpression.condition)
 
-                is UCallExpression -> {
-                    return if (isPermissionCheck(startingExpression))
-                        EnforcePermissionFix.fromCallExpression(startingExpression, context)
-                    else null
-                }
+                is UCallExpression -> return EnforcePermissionFix
+                            .fromCallExpression(context, startingExpression)
 
                 else -> null
             }
@@ -160,40 +154,5 @@
             ),
             enabledByDefault = false, // TODO: enable once b/241171714 is resolved
         )
-
-        private fun isPermissionCheck(callExpression: UCallExpression): Boolean {
-            val method = callExpression.resolve() ?: return false
-            val className = method.containingClass?.qualifiedName
-            return ENFORCE_PERMISSION_METHODS.any {
-                it.clazz == className && it.name == method.name
-            }
-        }
-
-        /**
-         * given a UMethod, determine if this method is
-         * an entrypoint to an interface generated by AIDL,
-         * returning the interface name if so
-         */
-        fun getContainingAidlInterface(node: UMethod): String? {
-            if (!isInClassCalledStub(node)) return null
-            for (superMethod in node.findSuperMethods()) {
-                for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
-                    ?: continue) {
-                    if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
-                        return superMethod.containingClass?.name
-                    }
-                }
-            }
-            return null
-        }
-
-        private fun isInClassCalledStub(node: UMethod): Boolean {
-            (node.containingClass as? PsiAnonymousClass)?.let {
-                return it.baseClassReference.referenceName == CLASS_STUB
-            }
-            return node.containingClass?.extendsList?.referenceElements?.any {
-                it.referenceName == CLASS_STUB
-            } ?: false
-        }
     }
 }
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
index 1a1c6bc..a968f5e 100644
--- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
+++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
@@ -19,6 +19,7 @@
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 
@@ -42,7 +43,7 @@
                         private Context mContext;
                         @Override
                         public void test() throws android.os.RemoteException {
-                            mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                         }
                     }
                 """
@@ -53,8 +54,8 @@
             .expect(
                 """
                 src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
-                        mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
-                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                 0 errors, 1 warnings
                 """
             )
@@ -62,9 +63,9 @@
                 """
                 Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
                 @@ -5 +5
-                +     @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS)
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
                 @@ -7 +8
-                -         mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                -         mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                 """
             )
     }
@@ -81,7 +82,7 @@
                             @Override
                             public void test() throws android.os.RemoteException {
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.READ_CONTACTS", "foo");
+                                    "android.permission.READ_CONTACTS", "foo");
                             }
                         };
                     }
@@ -102,10 +103,49 @@
                 """
                 Fix for src/Foo.java line 8: Annotate with @EnforcePermission:
                 @@ -6 +6
-                +         @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS)
+                +         @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
                 @@ -8 +9
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.READ_CONTACTS", "foo");
+                -                 "android.permission.READ_CONTACTS", "foo");
+                """
+            )
+    }
+
+    fun testConstantEvaluation() {
+        lint().files(
+            java(
+                """
+                    import android.content.Context;
+                    import android.test.ITest;
+
+                    public class Foo extends ITest.Stub {
+                        private Context mContext;
+                        @Override
+                        public void test() throws android.os.RemoteException {
+                            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+                        }
+                    }
+                """
+            ).indented(),
+            *stubs,
+            manifestStub
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+                @@ -6 +6
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+                @@ -8 +9
+                -         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
                 """
             )
     }
@@ -122,9 +162,9 @@
                             @Override
                             public void test() throws android.os.RemoteException {
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.READ_CONTACTS", "foo");
+                                    "android.permission.READ_CONTACTS", "foo");
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.WRITE_CONTACTS", "foo");
+                                    "android.permission.WRITE_CONTACTS", "foo");
                             }
                         };
                     }
@@ -144,13 +184,13 @@
             .expectFixDiffs(
                 """
                 Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
-                @@ -6 +6                                                                                                                                                                                                       
-                +         @android.annotation.EnforcePermission(allOf={android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS})
+                @@ -6 +6
+                +         @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
                 @@ -8 +9
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.READ_CONTACTS", "foo");
+                -                 "android.permission.READ_CONTACTS", "foo");
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.WRITE_CONTACTS", "foo");
+                -                 "android.permission.WRITE_CONTACTS", "foo");
                 """
             )
     }
@@ -166,7 +206,7 @@
                         @Override
                         public void test() throws android.os.RemoteException {
                             long uid = Binder.getCallingUid();
-                            mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                         }
                     }
                 """
@@ -177,6 +217,149 @@
             .expectClean()
     }
 
+    fun testPermissionHelper() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                    import android.content.Context;
+                    import android.test.ITest;
+
+                    public class Foo extends ITest.Stub {
+                        private Context mContext;
+
+                        @android.content.pm.PermissionMethod
+                        private void helper() {
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        }
+
+                        @Override
+                        public void test() throws android.os.RemoteException {
+                            helper();
+                        }
+                    }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        helper();
+                        ~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 14: Annotate with @EnforcePermission:
+                @@ -12 +12
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+                @@ -14 +15
+                -         helper();
+                """
+            )
+    }
+
+    fun testPermissionHelperAllOf() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+
+                    @android.content.pm.PermissionMethod
+                    private void helper() {
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo");
+                    }
+
+                    @Override
+                    public void test() throws android.os.RemoteException {
+                        helper();
+                        mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                    }
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:16: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 16: Annotate with @EnforcePermission:
+                @@ -13 +13
+                +     @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"})
+                @@ -15 +16
+                -         helper();
+                -         mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                """
+            )
+    }
+
+
+    fun testPermissionHelperNested() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+
+                    @android.content.pm.PermissionMethod
+                    private void helperHelper() {
+                        helper("android.permission.WRITE_CONTACTS");
+                    }
+
+                    @android.content.pm.PermissionMethod
+                    private void helper(@android.content.pm.PermissionName String extraPermission) {
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                    }
+
+                    @Override
+                    public void test() throws android.os.RemoteException {
+                        helperHelper();
+                    }
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:19: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        helperHelper();
+                        ~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 19: Annotate with @EnforcePermission:
+                @@ -17 +17
+                +     @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"})
+                @@ -19 +20
+                -         helperHelper();
+                """
+            )
+    }
+
+
+
     companion object {
         private val aidlStub: TestFile = java(
             """
@@ -192,7 +375,8 @@
             """
                 package android.content;
                 public class Context {
-                    public void enforceCallingOrSelfPermission(String permission, String message) {}
+                    @android.content.pm.PermissionMethod
+                    public void enforceCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {}
                 }
             """
         ).indented()
@@ -206,6 +390,59 @@
             """
         ).indented()
 
-        val stubs = arrayOf(aidlStub, contextStub, binderStub)
+        private val permissionMethodStub: TestFile = java(
+            """
+                package android.content.pm;
+
+                import static java.lang.annotation.ElementType.METHOD;
+                import static java.lang.annotation.RetentionPolicy.CLASS;
+
+                import java.lang.annotation.Retention;
+                import java.lang.annotation.Target;
+
+                @Retention(CLASS)
+                @Target({METHOD})
+                public @interface PermissionMethod {}
+            """
+        ).indented()
+
+        private val permissionNameStub: TestFile = java(
+            """
+                package android.content.pm;
+
+                import static java.lang.annotation.ElementType.FIELD;
+                import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+                import static java.lang.annotation.ElementType.METHOD;
+                import static java.lang.annotation.ElementType.PARAMETER;
+                import static java.lang.annotation.RetentionPolicy.CLASS;
+
+                import java.lang.annotation.Retention;
+                import java.lang.annotation.Target;
+
+                @Retention(CLASS)
+                @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
+                public @interface PermissionName {}
+            """
+        ).indented()
+
+        private val manifestStub: TestFile = java(
+            """
+                package android;
+
+                public final class Manifest {
+                    public static final class permission {
+                        public static final String READ_CONTACTS="android.permission.READ_CONTACTS";
+                    }
+                }
+            """.trimIndent()
+        )
+
+        val stubs = arrayOf(
+            aidlStub,
+            contextStub,
+            binderStub,
+            permissionMethodStub,
+            permissionNameStub
+        )
     }
 }